4 Grandcentral Dialer backend code
5 Eric Warnke <ericew@gmail.com>
17 from browser_emu import MozillaEmulator
20 _validateRe = re.compile("^[0-9]{10,}$")
23 class GCDialer(object):
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
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* """, re.M)
34 _inboxRe = re.compile(r"""<td>.*?(voicemail|received|missed|call return).*?</td>\s+<td>\s+<font size="2">\s+(.*?)\s+ \| \s+<a href="/mobile/contacts/.*?">(.*?)\s?</a>\s+<br/>\s+(.*?)\s?<a href=""", re.S)
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"
42 def __init__(self, cookieFile = None):
43 # Important items in this function are the setup of the browser emulation and cookie file
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()
53 self._accessToken = None
54 self._accountNum = None
55 self._callbackNumbers = {}
56 self._lastAuthed = 0.0
58 def isAuthed(self, force = False):
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
65 if time.time() - self._lastAuthed < 60 and not force:
69 forwardSelectionPage = self._browser.download(GCDialer._forwardselectURL)
70 except urllib2.URLError, e:
71 warnings.warn("%s is not accesible" % GCDialer._forwardselectURL, UserWarning, 2)
74 self._browser.cookies.save()
75 if GCDialer._isLoginPageRe.search(forwardSelectionPage) is None:
76 self._grabToken(forwardSelectionPage)
77 self._lastAuthed = time.time()
82 def login(self, username, password):
84 Attempt to login to grandcentral
85 @returns Whether login was successful or not
90 loginPostData = urllib.urlencode( {'username' : username , 'password' : password } )
93 loginSuccessOrFailurePage = self._browser.download(GCDialer._loginURL, loginPostData)
94 except urllib2.URLError, e:
95 warnings.warn("%s is not accesible" % GCDialer._loginURL, UserWarning, 2)
98 return self.isAuthed()
100 def dial(self, number):
102 This is the main function responsible for initating the callback
106 # If the number is not valid throw exception
107 if not self.is_valid_syntax(number):
108 raise ValueError('number is not valid')
110 # No point if we don't have the magic cookie
111 if not self.isAuthed():
112 self._msg = "Not authenticated"
115 # Strip leading 1 from 11 digit dialing
116 if len(number) == 11 and number[0] == 1:
120 callSuccessPage = self._browser.download(
121 GCDialer._clicktocallURL % (self._accessToken, number),
123 {'Referer' : 'http://www.grandcentral.com/mobile/messages'}
125 except urllib2.URLError, e:
126 warnings.warn("%s is not accesible" % GCDialer._clicktocallURL, UserWarning, 2)
129 if GCDialer._gcDialingStrRe.search(callSuccessPage) is not None:
132 self._msg = "Grand Central returned an error"
135 self._msg = "Unknown Error"
138 def clear_caches(self):
142 self._lastAuthed = 0.0
143 self._browser.cookies.clear()
144 self._browser.cookies.save()
146 def is_valid_syntax(self, number):
148 @returns If This number be called ( syntax validation only )
150 return _validateRe.match(number) is not None
153 def getAccountNumber(self):
155 @returns The grand central phone number
157 return self._accountNum
159 def setSaneCallback(self):
161 Try to set a sane default callback number on these preferences
162 1) 1747 numbers ( Gizmo )
163 2) anything with gizmo in the name
164 3) anything with computer in the name
167 print "setSaneCallback"
168 numbers = self.getCallbackNumbers()
170 for number, description in numbers.iteritems():
171 if not re.compile(r"""1747""").match(number) is None:
172 self.setCallbackNumber(number)
175 for number, description in numbers.iteritems():
176 if not re.compile(r"""gizmo""", re.I).search(description) is None:
177 self.setCallbackNumber(number)
180 for number, description in numbers.iteritems():
181 if not re.compile(r"""computer""", re.I).search(description) is None:
182 self.setCallbackNumber(number)
185 for number, description in numbers.iteritems():
186 self.setCallbackNumber(number)
189 def getCallbackNumbers(self):
191 @returns a dictionary mapping call back numbers to descriptions
192 @note These results are cached for 30 minutes.
194 print "getCallbackNumbers"
195 if time.time() - self._lastAuthed < 1800 or self.isAuthed():
196 return self._callbackNumbers
200 def setCallbackNumber(self, callbacknumber):
202 Set the number that grandcental calls
203 @param callbacknumber should be a proper 10 digit number
205 print "setCallbackNumber %s" % (callbacknumber)
207 callbackPostData = urllib.urlencode({'a_t' : self._accessToken, 'default_number' : callbacknumber })
209 callbackSetPage = self._browser.download(GCDialer._setforwardURL, callbackPostData)
210 except urllib2.URLError, e:
211 warnings.warn("%s is not accesible" % GCDialer._setforwardURL, UserWarning, 2)
214 self._browser.cookies.save()
217 def getCallbackNumber(self):
219 @returns Current callback number or None
221 for c in self._browser.cookies:
222 if c.name == "pda_forwarding_number":
226 def get_recent(self):
228 @returns Iterable of (personsName, phoneNumber, date, action)
231 recentCallsPage = self._browser.download(GCDialer._inboxallURL)
232 except urllib2.URLError, e:
233 warnings.warn("%s is not accesible" % GCDialer._inboxallURL, UserWarning, 2)
236 for match in self._inboxRe.finditer(recentCallsPage):
237 phoneNumber = match.group(4)
238 action = match.group(1)
239 date = match.group(2)
240 personsName = match.group(3)
241 yield personsName, phoneNumber, date, action
243 def _grabToken(self, data):
244 "Pull the magic cookie from the datastream"
245 atGroup = GCDialer._accessTokenRe.search(data)
246 self._accessToken = atGroup.group(1)
248 anGroup = GCDialer._accountNumRe.search(data)
249 self._accountNum = anGroup.group(1)
251 self._callbackNumbers = {}
252 for match in GCDialer._callbackRe.finditer(data):
253 self._callbackNumbers[match.group(1)] = match.group(2)