Message type selection stuff
[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 from __future__ import with_statement
29
30 import os
31 import re
32 import urllib
33 import urllib2
34 import time
35 import datetime
36 import itertools
37 import logging
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 _moduleLogger = logging.getLogger("gvoice.dialer")
51 _TRUE_REGEX = re.compile("true")
52 _FALSE_REGEX = re.compile("false")
53
54
55 def safe_eval(s):
56         s = _TRUE_REGEX.sub("True", s)
57         s = _FALSE_REGEX.sub("False", s)
58         return eval(s, {}, {})
59
60
61 if simplejson is None:
62         def parse_json(flattened):
63                 return safe_eval(flattened)
64 else:
65         def parse_json(flattened):
66                 return simplejson.loads(flattened)
67
68
69 def itergroup(iterator, count, padValue = None):
70         """
71         Iterate in groups of 'count' values. If there
72         aren't enough values, the last result is padded with
73         None.
74
75         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
76         ...     print tuple(val)
77         (1, 2, 3)
78         (4, 5, 6)
79         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
80         ...     print list(val)
81         [1, 2, 3]
82         [4, 5, 6]
83         >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
84         ...     print tuple(val)
85         (1, 2, 3)
86         (4, 5, 6)
87         (7, None, None)
88         >>> for val in itergroup("123456", 3):
89         ...     print tuple(val)
90         ('1', '2', '3')
91         ('4', '5', '6')
92         >>> for val in itergroup("123456", 3):
93         ...     print repr("".join(val))
94         '123'
95         '456'
96         """
97         paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
98         nIterators = (paddedIterator, ) * count
99         return itertools.izip(*nIterators)
100
101
102 class NetworkError(RuntimeError):
103         pass
104
105
106 class GVDialer(object):
107         """
108         This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
109         the functions include login, setting up a callback number, and initalting a callback
110         """
111
112         def __init__(self, cookieFile = None):
113                 # Important items in this function are the setup of the browser emulation and cookie file
114                 self._browser = browser_emu.MozillaEmulator(1)
115                 if cookieFile is None:
116                         cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
117                 self._browser.cookies.filename = cookieFile
118                 if os.path.isfile(cookieFile):
119                         self._browser.cookies.load()
120
121                 self._token = ""
122                 self._accountNum = ""
123                 self._lastAuthed = 0.0
124                 self._callbackNumber = ""
125                 self._callbackNumbers = {}
126
127         _forwardURL = "https://www.google.com/voice/mobile/phones"
128
129         def is_authed(self, force = False):
130                 """
131                 Attempts to detect a current session
132                 @note Once logged in try not to reauth more than once a minute.
133                 @returns If authenticated
134                 """
135                 if (time.time() - self._lastAuthed) < 120 and not force:
136                         return True
137
138                 try:
139                         page = self._browser.download(self._forwardURL)
140                         self._grab_account_info(page)
141                 except Exception, e:
142                         _moduleLogger.exception(str(e))
143                         return False
144
145                 self._browser.cookies.save()
146                 self._lastAuthed = time.time()
147                 return True
148
149         _tokenURL = "http://www.google.com/voice/m"
150         _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
151         _galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
152
153         def _get_token(self):
154                 try:
155                         tokenPage = self._browser.download(self._tokenURL)
156                 except urllib2.URLError, e:
157                         _moduleLogger.exception("Translating error: %s" % str(e))
158                         raise NetworkError("%s is not accesible" % self._loginURL)
159                 galxTokens = self._galxRe.search(tokenPage)
160                 if galxTokens is not None:
161                         galxToken = galxTokens.group(1)
162                 else:
163                         galxToken = ""
164                         _moduleLogger.debug("Could not grab GALX token")
165                 return galxToken
166
167         def _login(self, username, password, token):
168                 loginPostData = urllib.urlencode({
169                         'Email' : username,
170                         'Passwd' : password,
171                         'service': "grandcentral",
172                         "ltmpl": "mobile",
173                         "btmpl": "mobile",
174                         "PersistentCookie": "yes",
175                         "GALX": token,
176                         "continue": self._forwardURL,
177                 })
178
179                 try:
180                         loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
181                 except urllib2.URLError, e:
182                         _moduleLogger.exception("Translating error: %s" % str(e))
183                         raise NetworkError("%s is not accesible" % self._loginURL)
184                 return loginSuccessOrFailurePage
185
186         def login(self, username, password):
187                 """
188                 Attempt to login to GoogleVoice
189                 @returns Whether login was successful or not
190                 """
191                 self.logout()
192                 galxToken = self._get_token()
193                 loginSuccessOrFailurePage = self._login(username, password, galxToken)
194
195                 try:
196                         self._grab_account_info(loginSuccessOrFailurePage)
197                 except Exception, e:
198                         # Retry in case the redirect failed
199                         # luckily is_authed does everything we need for a retry
200                         loggedIn = self.is_authed(True)
201                         if not loggedIn:
202                                 _moduleLogger.exception(str(e))
203                                 return False
204                         _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
205
206                 self._browser.cookies.save()
207                 self._lastAuthed = time.time()
208                 return True
209
210         def logout(self):
211                 self._lastAuthed = 0.0
212                 self._browser.cookies.clear()
213                 self._browser.cookies.save()
214
215         _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
216         _clicktocallURL = "https://www.google.com/voice/m/sendcall"
217
218         def dial(self, number):
219                 """
220                 This is the main function responsible for initating the callback
221                 """
222                 number = self._send_validation(number)
223                 try:
224                         clickToCallData = urllib.urlencode({
225                                 "number": number,
226                                 "phone": self._callbackNumber,
227                                 "_rnr_se": self._token,
228                         })
229                         otherData = {
230                                 'Referer' : 'https://google.com/voice/m/callsms',
231                         }
232                         callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
233                 except urllib2.URLError, e:
234                         _moduleLogger.exception("Translating error: %s" % str(e))
235                         raise NetworkError("%s is not accesible" % self._clicktocallURL)
236
237                 if self._gvDialingStrRe.search(callSuccessPage) is None:
238                         raise RuntimeError("Google Voice returned an error")
239
240                 return True
241
242         _sendSmsURL = "https://www.google.com/voice/m/sendsms"
243
244         def send_sms(self, number, message):
245                 number = self._send_validation(number)
246                 try:
247                         smsData = urllib.urlencode({
248                                 "number": number,
249                                 "smstext": message,
250                                 "_rnr_se": self._token,
251                                 "id": "undefined",
252                                 "c": "undefined",
253                         })
254                         otherData = {
255                                 'Referer' : 'https://google.com/voice/m/sms',
256                         }
257                         smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
258                 except urllib2.URLError, e:
259                         _moduleLogger.exception("Translating error: %s" % str(e))
260                         raise NetworkError("%s is not accesible" % self._sendSmsURL)
261
262                 return True
263
264         _validateRe = re.compile("^[0-9]{10,}$")
265
266         def is_valid_syntax(self, number):
267                 """
268                 @returns If This number be called ( syntax validation only )
269                 """
270                 return self._validateRe.match(number) is not None
271
272         def get_account_number(self):
273                 """
274                 @returns The GoogleVoice phone number
275                 """
276                 return self._accountNum
277
278         def get_callback_numbers(self):
279                 """
280                 @returns a dictionary mapping call back numbers to descriptions
281                 @note These results are cached for 30 minutes.
282                 """
283                 if not self.is_authed():
284                         return {}
285                 return self._callbackNumbers
286
287         def set_callback_number(self, callbacknumber):
288                 """
289                 Set the number that GoogleVoice calls
290                 @param callbacknumber should be a proper 10 digit number
291                 """
292                 self._callbackNumber = callbacknumber
293                 return True
294
295         def get_callback_number(self):
296                 """
297                 @returns Current callback number or None
298                 """
299                 return self._callbackNumber
300
301         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
302         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
303         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
304         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
305
306         def get_recent(self):
307                 """
308                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
309                 """
310                 for action, url in (
311                         ("Received", self._receivedCallsURL),
312                         ("Missed", self._missedCallsURL),
313                         ("Placed", self._placedCallsURL),
314                 ):
315                         try:
316                                 flatXml = self._browser.download(url)
317                         except urllib2.URLError, e:
318                                 _moduleLogger.exception("Translating error: %s" % str(e))
319                                 raise NetworkError("%s is not accesible" % url)
320
321                         allRecentHtml = self._grab_html(flatXml)
322                         allRecentData = self._parse_voicemail(allRecentHtml)
323                         for recentCallData in allRecentData:
324                                 recentCallData["action"] = action
325                                 yield recentCallData
326
327         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
328         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
329         _contactsURL = "https://www.google.com/voice/mobile/contacts"
330
331         def get_contacts(self):
332                 """
333                 @returns Iterable of (contact id, contact name)
334                 """
335                 contactsPagesUrls = [self._contactsURL]
336                 for contactsPageUrl in contactsPagesUrls:
337                         try:
338                                 contactsPage = self._browser.download(contactsPageUrl)
339                         except urllib2.URLError, e:
340                                 _moduleLogger.exception("Translating error: %s" % str(e))
341                                 raise NetworkError("%s is not accesible" % contactsPageUrl)
342                         for contact_match in self._contactsRe.finditer(contactsPage):
343                                 contactId = contact_match.group(1)
344                                 contactName = saxutils.unescape(contact_match.group(2))
345                                 contact = contactId, contactName
346                                 yield contact
347
348                         next_match = self._contactsNextRe.match(contactsPage)
349                         if next_match is not None:
350                                 newContactsPageUrl = self._contactsURL + next_match.group(1)
351                                 contactsPagesUrls.append(newContactsPageUrl)
352
353         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
354         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
355
356         def get_contact_details(self, contactId):
357                 """
358                 @returns Iterable of (Phone Type, Phone Number)
359                 """
360                 try:
361                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
362                 except urllib2.URLError, e:
363                         _moduleLogger.exception("Translating error: %s" % str(e))
364                         raise NetworkError("%s is not accesible" % self._contactDetailURL)
365
366                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
367                         phoneNumber = detail_match.group(1)
368                         phoneType = saxutils.unescape(detail_match.group(2))
369                         yield (phoneType, phoneNumber)
370
371         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
372         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
373
374         @staticmethod
375         def _merge_messages(parsedMessages, json):
376                 for message in parsedMessages:
377                         id = message["id"]
378                         jsonItem = json["messages"][id]
379                         message["isRead"] = jsonItem["isRead"]
380                         message["isSpam"] = jsonItem["isSpam"]
381                         message["isTrash"] = jsonItem["isTrash"]
382                         yield message
383
384         def get_messages(self):
385                 try:
386                         voicemailPage = self._browser.download(self._voicemailURL)
387                 except urllib2.URLError, e:
388                         _moduleLogger.exception("Translating error: %s" % str(e))
389                         raise NetworkError("%s is not accesible" % self._voicemailURL)
390                 voicemailHtml = self._grab_html(voicemailPage)
391                 voicemailJson = self._grab_json(voicemailPage)
392                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
393                 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
394                 decoratedVoicemails = self._decorate_voicemail(voicemails)
395
396                 try:
397                         smsPage = self._browser.download(self._smsURL)
398                 except urllib2.URLError, e:
399                         _moduleLogger.exception("Translating error: %s" % str(e))
400                         raise NetworkError("%s is not accesible" % self._smsURL)
401                 smsHtml = self._grab_html(smsPage)
402                 smsJson = self._grab_json(smsPage)
403                 parsedSms = self._parse_sms(smsHtml)
404                 smss = self._merge_messages(parsedSms, smsJson)
405                 decoratedSms = self._decorate_sms(smss)
406
407                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
408                 return allMessages
409
410         def clear_caches(self):
411                 pass
412
413         def get_addressbooks(self):
414                 """
415                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
416                 """
417                 yield self, "", ""
418
419         def open_addressbook(self, bookId):
420                 return self
421
422         @staticmethod
423         def contact_source_short_name(contactId):
424                 return "GV"
425
426         @staticmethod
427         def factory_name():
428                 return "Google Voice"
429
430         def _grab_json(self, flatXml):
431                 xmlTree = ElementTree.fromstring(flatXml)
432                 jsonElement = xmlTree.getchildren()[0]
433                 flatJson = jsonElement.text
434                 jsonTree = parse_json(flatJson)
435                 return jsonTree
436
437         def _grab_html(self, flatXml):
438                 xmlTree = ElementTree.fromstring(flatXml)
439                 htmlElement = xmlTree.getchildren()[1]
440                 flatHtml = htmlElement.text
441                 return flatHtml
442
443         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
444         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
445         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
446
447         def _grab_account_info(self, page):
448                 tokenGroup = self._tokenRe.search(page)
449                 if tokenGroup is None:
450                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
451                 self._token = tokenGroup.group(1)
452
453                 anGroup = self._accountNumRe.search(page)
454                 if anGroup is not None:
455                         self._accountNum = anGroup.group(1)
456                 else:
457                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
458
459                 self._callbackNumbers = {}
460                 for match in self._callbackRe.finditer(page):
461                         callbackNumber = match.group(2)
462                         callbackName = match.group(1)
463                         self._callbackNumbers[callbackNumber] = callbackName
464                 if len(self._callbackNumbers) == 0:
465                         _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
466
467         def _send_validation(self, number):
468                 if not self.is_valid_syntax(number):
469                         raise ValueError('Number is not valid: "%s"' % number)
470                 elif not self.is_authed():
471                         raise RuntimeError("Not Authenticated")
472
473                 if len(number) == 11 and number[0] == 1:
474                         # Strip leading 1 from 11 digit dialing
475                         number = number[1:]
476                 return number
477
478         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
479         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
480         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
481         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
482         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
483         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
484         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
485         _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
486         #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
487         #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
488         _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
489
490         @staticmethod
491         def _interpret_voicemail_regex(group):
492                 quality, content, number = group.group(2), group.group(3), group.group(4)
493                 if quality is not None and content is not None:
494                         return quality, content
495                 elif number is not None:
496                         return "high", number
497
498         def _parse_voicemail(self, voicemailHtml):
499                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
500                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
501                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
502                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
503                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
504                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
505                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
506                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
507                         location = locationGroup.group(1).strip() if locationGroup else ""
508
509                         nameGroup = self._voicemailNameRegex.search(messageHtml)
510                         name = nameGroup.group(1).strip() if nameGroup else ""
511                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
512                         number = numberGroup.group(1).strip() if numberGroup else ""
513                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
514                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
515                         contactIdGroup = self._messagesContactID.search(messageHtml)
516                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
517
518                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
519                         messageParts = (
520                                 self._interpret_voicemail_regex(group)
521                                 for group in messageGroups
522                         ) if messageGroups else ()
523
524                         yield {
525                                 "id": messageId.strip(),
526                                 "contactId": contactId,
527                                 "name": name,
528                                 "time": exactTime,
529                                 "relTime": relativeTime,
530                                 "prettyNumber": prettyNumber,
531                                 "number": number,
532                                 "location": location,
533                                 "messageParts": messageParts,
534                                 "type": "Voicemail",
535                         }
536
537         def _decorate_voicemail(self, parsedVoicemails):
538                 messagePartFormat = {
539                         "med1": "<i>%s</i>",
540                         "med2": "%s",
541                         "high": "<b>%s</b>",
542                 }
543                 for voicemailData in parsedVoicemails:
544                         message = " ".join((
545                                 messagePartFormat[quality] % part
546                                 for (quality, part) in voicemailData["messageParts"]
547                         )).strip()
548                         if not message:
549                                 message = "No Transcription"
550                         whoFrom = voicemailData["name"]
551                         when = voicemailData["time"]
552                         voicemailData["messageParts"] = ((whoFrom, message, when), )
553                         yield voicemailData
554
555         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
556         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
557         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
558
559         def _parse_sms(self, smsHtml):
560                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
561                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
562                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
563                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
564                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
565                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
566                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
567
568                         nameGroup = self._voicemailNameRegex.search(messageHtml)
569                         name = nameGroup.group(1).strip() if nameGroup else ""
570                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
571                         number = numberGroup.group(1).strip() if numberGroup else ""
572                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
573                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
574                         contactIdGroup = self._messagesContactID.search(messageHtml)
575                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
576
577                         fromGroups = self._smsFromRegex.finditer(messageHtml)
578                         fromParts = (group.group(1).strip() for group in fromGroups)
579                         textGroups = self._smsTextRegex.finditer(messageHtml)
580                         textParts = (group.group(1).strip() for group in textGroups)
581                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
582                         timeParts = (group.group(1).strip() for group in timeGroups)
583
584                         messageParts = itertools.izip(fromParts, textParts, timeParts)
585
586                         yield {
587                                 "id": messageId.strip(),
588                                 "contactId": contactId,
589                                 "name": name,
590                                 "time": exactTime,
591                                 "relTime": relativeTime,
592                                 "prettyNumber": prettyNumber,
593                                 "number": number,
594                                 "location": "",
595                                 "messageParts": messageParts,
596                                 "type": "Texts",
597                         }
598
599         def _decorate_sms(self, parsedTexts):
600                 return parsedTexts
601
602
603 def set_sane_callback(backend):
604         """
605         Try to set a sane default callback number on these preferences
606         1) 1747 numbers ( Gizmo )
607         2) anything with gizmo in the name
608         3) anything with computer in the name
609         4) the first value
610         """
611         numbers = backend.get_callback_numbers()
612
613         priorityOrderedCriteria = [
614                 ("1747", None),
615                 (None, "gizmo"),
616                 (None, "computer"),
617                 (None, "sip"),
618                 (None, None),
619         ]
620
621         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
622                 for number, description in numbers.iteritems():
623                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
624                                 continue
625                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
626                                 continue
627                         backend.set_callback_number(number)
628                         return
629
630
631 def sort_messages(allMessages):
632         sortableAllMessages = [
633                 (message["time"], message)
634                 for message in allMessages
635         ]
636         sortableAllMessages.sort(reverse=True)
637         return (
638                 message
639                 for (exactTime, message) in sortableAllMessages
640         )
641
642
643 def decorate_recent(recentCallData):
644         """
645         @returns (personsName, phoneNumber, date, action)
646         """
647         contactId = recentCallData["contactId"]
648         if recentCallData["name"]:
649                 header = recentCallData["name"]
650         elif recentCallData["prettyNumber"]:
651                 header = recentCallData["prettyNumber"]
652         elif recentCallData["location"]:
653                 header = recentCallData["location"]
654         else:
655                 header = "Unknown"
656
657         number = recentCallData["number"]
658         relTime = recentCallData["relTime"]
659         action = recentCallData["action"]
660         return contactId, header, number, relTime, action
661
662
663 def decorate_message(messageData):
664         contactId = messageData["contactId"]
665         exactTime = messageData["time"]
666         if messageData["name"]:
667                 header = messageData["name"]
668         elif messageData["prettyNumber"]:
669                 header = messageData["prettyNumber"]
670         else:
671                 header = "Unknown"
672         number = messageData["number"]
673         relativeTime = messageData["relTime"]
674
675         messageParts = list(messageData["messageParts"])
676         if len(messageParts) == 0:
677                 messages = ("No Transcription", )
678         elif len(messageParts) == 1:
679                 messages = (messageParts[0][1], )
680         else:
681                 messages = [
682                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
683                         for messagePart in messageParts
684                 ]
685
686         decoratedResults = contactId, header, number, relativeTime, messages
687         return decoratedResults
688
689
690 def test_backend(username, password):
691         backend = GVDialer()
692         print "Authenticated: ", backend.is_authed()
693         if not backend.is_authed():
694                 print "Login?: ", backend.login(username, password)
695         print "Authenticated: ", backend.is_authed()
696
697         print "Token: ", backend._token
698         #print "Account: ", backend.get_account_number()
699         #print "Callback: ", backend.get_callback_number()
700         #print "All Callback: ",
701         import pprint
702         #pprint.pprint(backend.get_callback_numbers())
703
704         #print "Recent: "
705         #for data in backend.get_recent():
706         #       pprint.pprint(data)
707         #for data in sort_messages(backend.get_recent()):
708         #       pprint.pprint(decorate_recent(data))
709         #pprint.pprint(list(backend.get_recent()))
710
711         #print "Contacts: ",
712         #for contact in backend.get_contacts():
713         #       print contact
714         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
715
716         print "Messages: ",
717         for message in backend.get_messages():
718                 message["messageParts"] = list(message["messageParts"])
719                 pprint.pprint(message)
720         #for message in sort_messages(backend.get_messages()):
721         #       pprint.pprint(decorate_message(message))
722
723         return backend
724
725
726 _TEST_WEBPAGES = [
727         ("forward", GVDialer._forwardURL),
728         ("token", GVDialer._tokenURL),
729         ("login", GVDialer._loginURL),
730         ("contacts", GVDialer._contactsURL),
731
732         ("voicemail", GVDialer._voicemailURL),
733         ("sms", GVDialer._smsURL),
734
735         ("recent", GVDialer._recentCallsURL),
736         ("placed", GVDialer._placedCallsURL),
737         ("recieved", GVDialer._receivedCallsURL),
738         ("missed", GVDialer._missedCallsURL),
739 ]
740
741
742 def grab_debug_info(username, password):
743         cookieFile = os.path.join(".", "raw_cookies.txt")
744         try:
745                 os.remove(cookieFile)
746         except OSError:
747                 pass
748
749         backend = GVDialer(cookieFile)
750         browser = backend._browser
751
752         # Get Pages
753         print "Grabbing pre-login pages"
754         for name, url in _TEST_WEBPAGES:
755                 try:
756                         page = browser.download(url)
757                 except StandardError, e:
758                         print e.message
759                         continue
760                 print "\tWriting to file"
761                 with open("not_loggedin_%s.txt" % name, "w") as f:
762                         f.write(page)
763
764         # Login
765         print "Attempting login"
766         galxToken = backend._get_token()
767         loginSuccessOrFailurePage = backend._login(username, password, galxToken)
768         with open("loggingin.txt", "w") as f:
769                 print "\tWriting to file"
770                 f.write(loginSuccessOrFailurePage)
771         try:
772                 backend._grab_account_info(loginSuccessOrFailurePage)
773         except Exception:
774                 # Retry in case the redirect failed
775                 # luckily is_authed does everything we need for a retry
776                 loggedIn = backend.is_authed(True)
777                 if not loggedIn:
778                         raise
779
780         # Get Pages
781         print "Grabbing post-login pages"
782         for name, url in _TEST_WEBPAGES:
783                 try:
784                         page = browser.download(url)
785                 except StandardError, e:
786                         print e.message
787                         continue
788                 print "\tWriting to file"
789                 with open("loggedin_%s.txt" % name, "w") as f:
790                         f.write(page)
791
792         # Cookies
793         browser.cookies.save()
794         print "\tWriting cookies to file"
795         with open("cookies.txt", "w") as f:
796                 f.writelines(
797                         "%s: %s\n" % (c.name, c.value)
798                         for c in browser.cookies
799                 )
800
801
802 if __name__ == "__main__":
803         import sys
804         logging.basicConfig(level=logging.DEBUG)
805         #test_backend(sys.argv[1], sys.argv[2])
806         grab_debug_info(sys.argv[1], sys.argv[2])