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