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