* Improved error reporting
[gc-dialer] / gc_dialer / gcbackend.py
1 #!/usr/bin/python
2
3 """
4 Grandcentral Dialer backend code
5 Eric Warnke <ericew@gmail.com>
6 Copyright 2008 GPLv2
7 """
8
9
10 import os
11 import re
12 import urllib
13 import time
14
15 from browser_emu import MozillaEmulator
16
17
18 class GCDialer(object):
19         """
20         This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
21         the functions include login, setting up a callback number, and initalting a callback
22         """
23
24         _gcDialingStrRe = re.compile("This may take a few seconds", re.M)       # string from Grandcentral.com on successful dial
25         _validateRe     = re.compile("^[0-9]{10,}$")
26         _accessTokenRe  = re.compile(r"""<input type="hidden" name="a_t" [^>]*value="(.*)"/>""")
27         _isLoginPageRe  = re.compile(r"""<form method="post" action="https://www.grandcentral.com/mobile/account/login">""")
28         _callbackRe     = re.compile(r"""name="default_number" value="(\d+)" />\s+(.*)\s$""", re.M)
29         _accountNumRe   = re.compile(r"""<img src="/images/mobile/inbox_logo.gif" alt="GrandCentral" />\s*(.{14})\s*&nbsp""", re.M)
30         _inboxRe        = re.compile(r"""<td>.*?(voicemail|received|missed|call return).*?</td>\s+<td>\s+<font size="2">\s+(.*?)\s+&nbsp;\|&nbsp;\s+<a href="/mobile/contacts/.*?">(.*?)\s?</a>\s+<br/>\s+(.*?)\s?<a href=""", re.S)
31
32         _forwardselectURL       = "http://www.grandcentral.com/mobile/settings/forwarding_select"
33         _loginURL               = "https://www.grandcentral.com/mobile/account/login"
34         _setforwardURL          = "http://www.grandcentral.com/mobile/settings/set_forwarding?from=settings"
35         _clicktocallURL         = "http://www.grandcentral.com/mobile/calls/click_to_call?a_t=%s&destno=%s"
36         _inboxallURL            = "http://www.grandcentral.com/mobile/messages/inbox?types=all"
37
38         def __init__(self, cookieFile = None):
39                 # Important items in this function are the setup of the browser emulation and cookie file
40                 self._msg = ""
41                 if cookieFile is None:
42                         cookieFile = os.path.join(os.path.expanduser("~"), ".gc_dialer_cookies.txt")
43                 self._browser = MozillaEmulator(None, 0)
44                 self._browser.cookies.filename = cookieFile
45                 if os.path.isfile(cookieFile):
46                         self._browser.cookies.load()
47                 #else:
48                 #       self._browser.cookies.save()
49                 self._lastData = ""
50                 self._accessToken = None
51                 self._accountNum = None
52                 self._callbackNumbers = {}
53                 self._lastAuthed = 0.0
54
55         def grabToken(self, data):
56                 "Pull the magic cookie from the datastream"
57                 atGroup = GCDialer._accessTokenRe.search(data)
58                 try:
59                         self._accessToken = atGroup.group(1)
60                 except:
61                         pass
62
63                 anGroup = GCDialer._accountNumRe.search(data)
64                 try:
65                         self._accountNum = anGroup.group(1)
66                 except:
67                         pass
68
69                 self._callbackNumbers = {}
70                 try:
71                         for match in GCDialer._callbackRe.finditer(data):
72                                 self._callbackNumbers[match.group(1)] = match.group(2)
73                 except:
74                         pass
75
76         def getAccountNumber(self):
77                 return self._accountNum
78
79         def isAuthed(self, force = False):
80                 """
81                 Attempts to detect a current session and pull the
82                 auth token ( a_t ) from the page.  Once logged in
83                 try not to reauth more than once a minute.
84                 """
85         
86
87                 if time.time() - self._lastAuthed < 60 and not force:
88                         return True
89
90                 try:    
91                         self._lastData = self._browser.download(GCDialer._forwardselectURL)
92                         self._browser.cookies.save()
93                         if GCDialer._isLoginPageRe.search(self._lastData) is None:
94                                 self.grabToken(self._lastData)
95                                 self._lastAuthed = time.time()
96                                 return True
97                 except:
98                         pass
99                 return False
100
101         def login(self, username, password):
102                 """
103                 Attempt to login to grandcentral
104                 """
105                 try:
106                         if self.isAuthed():
107                                 return
108                         loginPostData = urllib.urlencode( {'username' : username , 'password' : password } )
109                         self._lastData = self._browser.download(GCDialer._loginURL, loginPostData)
110                         return self.isAuthed()
111                 except:
112                         pass
113                 return False
114
115         def setSaneCallback(self):
116                 """
117                 Try to set a sane default callback number on these preferences
118                 1) 1747 numbers ( Gizmo )
119                 2) anything with gizmo in the name
120                 3) anything with computer in the name
121                 4) the first value
122                 """
123                 print "setSaneCallback"
124                 numbers = self.getCallbackNumbers()
125
126                 for number, description in numbers.iteritems():
127                         if not re.compile(r"""1747""").match(number) is None:
128                                 self.setCallbackNumber(number)
129                                 return
130
131                 for number, description in numbers.iteritems():
132                         if not re.compile(r"""gizmo""", re.I).search(description) is None:
133                                 self.setCallbackNumber(number)
134                                 return
135
136                 for number, description in numbers.iteritems():
137                         if not re.compile(r"""computer""", re.I).search(description) is None:
138                                 self.setCallbackNumber(number)
139                                 return
140
141                 for number, description in numbers.iteritems():
142                         self.setCallbackNumber(number)
143                         return
144
145         def getCallbackNumbers(self):
146                 """
147                 @returns a dictionary mapping call back numbers to descriptions. These results
148                 are cached for 30 minutes.
149                 """
150                 print "getCallbackNumbers"
151                 if time.time() - self._lastAuthed < 1800 or self.isAuthed():
152                         return self._callbackNumbers
153
154                 return {}
155
156         def setCallbackNumber(self, callbacknumber):
157                 """
158                 set the number that grandcental calls
159                 this should be a proper 10 digit number
160                 """
161                 print "setCallbackNumber %s" % (callbacknumber)
162                 try:
163                         callbackPostData = urllib.urlencode({'a_t' : self._accessToken, 'default_number' : callbacknumber })
164                         self._lastData = self._browser.download(GCDialer._setforwardURL, callbackPostData)
165                         self._browser.cookies.save()
166                 except:
167                         pass
168
169         def getCallbackNumber(self):
170                 for c in self._browser.cookies:
171                         if c.name == "pda_forwarding_number":
172                                 return c.value
173                 return None
174
175         def reset(self):
176                 self._lastAuthed = 0.0
177                 self._browser.cookies.clear()
178                 self._browser.cookies.save()
179
180         def validate(self, number):
181                 """
182                 Can this number be called ( syntax validation only )
183                 """
184                 return GCDialer._validateRe.match(number) is not None
185
186         def dial(self, number):
187                 """
188                 This is the main function responsible for initating the callback
189                 """
190                 self._msg = ""
191
192                 # If the number is not valid throw exception
193                 if self.validate(number) is False:
194                         raise ValueError('number is not valid')
195
196                 # No point if we don't have the magic cookie
197                 if not self.isAuthed():
198                         self._msg = "Not authenticated"
199                         return False
200
201                 # Strip leading 1 from 11 digit dialing
202                 if len(number) == 11 and number[0] == 1:
203                         number = number[1:]
204
205                 try:
206                         self._lastData = self._browser.download(
207                                 GCDialer._clicktocallURL % (self._accessToken, number),
208                                 None, {'Referer' : 'http://www.grandcentral.com/mobile/messages'} )
209
210                         if GCDialer._gcDialingStrRe.search(self._lastData) is not None:
211                                 return True
212                         else:
213                                 self._msg = "Grand Central returned an error"
214                                 return False
215                 except:
216                         pass
217         
218                 self._msg = "Unknown Error"
219                 return False
220
221         def get_recent(self):
222                 try:
223                         self._lastData = self._browser.download(GCDialer._inboxallURL)
224                         for match in self._inboxRe.finditer(self._lastData):
225                                 yield (match.group(4), "%s on %s from/to %s - %s" % (match.group(1).capitalize(), match.group(2), match.group(3), match.group(4)))
226                 except:
227                         pass