4 Grandcentral Dialer backend code
5 Eric Warnke <ericew@gmail.com>
15 from browser_emu import MozillaEmulator
18 class GCDialer(object):
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
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* """, re.M)
30 _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)
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"
38 def __init__(self, cookieFile = None):
39 # Important items in this function are the setup of the browser emulation and cookie file
42 self._browser = MozillaEmulator(None, 0)
43 if cookieFile is None:
44 cookieFile = os.path.join(os.path.expanduser("~"), ".gc_dialer_cookies.txt")
45 self._browser.cookies.filename = cookieFile
46 if os.path.isfile(cookieFile):
47 self._browser.cookies.load()
49 self._accessToken = None
50 self._accountNum = None
51 self._callbackNumbers = {}
52 self._lastAuthed = 0.0
54 def isAuthed(self, force = False):
56 Attempts to detect a current session and pull the
57 auth token ( a_t ) from the page. Once logged in
58 try not to reauth more than once a minute.
61 if time.time() - self._lastAuthed < 60 and not force:
65 forwardSelectionPage = self._browser.download(GCDialer._forwardselectURL)
66 self._browser.cookies.save()
67 if GCDialer._isLoginPageRe.search(forwardSelectionPage) is None:
68 self._grabToken(forwardSelectionPage)
69 self._lastAuthed = time.time()
75 def login(self, username, password):
77 Attempt to login to grandcentral
82 loginPostData = urllib.urlencode( {'username' : username , 'password' : password } )
83 loginSuccessOrFailurePage = self._browser.download(GCDialer._loginURL, loginPostData)
84 return self.isAuthed()
89 def dial(self, number):
91 This is the main function responsible for initating the callback
95 # If the number is not valid throw exception
96 if self.validate(number) is False:
97 raise ValueError('number is not valid')
99 # No point if we don't have the magic cookie
100 if not self.isAuthed():
101 self._msg = "Not authenticated"
104 # Strip leading 1 from 11 digit dialing
105 if len(number) == 11 and number[0] == 1:
109 callSuccessPage = self._browser.download(
110 GCDialer._clicktocallURL % (self._accessToken, number),
111 None, {'Referer' : 'http://www.grandcentral.com/mobile/messages'} )
113 if GCDialer._gcDialingStrRe.search(callSuccessPage) is not None:
116 self._msg = "Grand Central returned an error"
121 self._msg = "Unknown Error"
124 def clear_caches(self):
131 self._lastAuthed = 0.0
132 self._browser.cookies.clear()
133 self._browser.cookies.save()
135 def getAccountNumber(self):
136 return self._accountNum
138 def validate(self, number):
140 Can this number be called ( syntax validation only )
142 return GCDialer._validateRe.match(number) is not None
144 def setSaneCallback(self):
146 Try to set a sane default callback number on these preferences
147 1) 1747 numbers ( Gizmo )
148 2) anything with gizmo in the name
149 3) anything with computer in the name
152 print "setSaneCallback"
153 numbers = self.getCallbackNumbers()
155 for number, description in numbers.iteritems():
156 if not re.compile(r"""1747""").match(number) is None:
157 self.setCallbackNumber(number)
160 for number, description in numbers.iteritems():
161 if not re.compile(r"""gizmo""", re.I).search(description) is None:
162 self.setCallbackNumber(number)
165 for number, description in numbers.iteritems():
166 if not re.compile(r"""computer""", re.I).search(description) is None:
167 self.setCallbackNumber(number)
170 for number, description in numbers.iteritems():
171 self.setCallbackNumber(number)
174 def getCallbackNumbers(self):
176 @returns a dictionary mapping call back numbers to descriptions. These results
177 are cached for 30 minutes.
179 print "getCallbackNumbers"
180 if time.time() - self._lastAuthed < 1800 or self.isAuthed():
181 return self._callbackNumbers
185 def setCallbackNumber(self, callbacknumber):
187 set the number that grandcental calls
188 this should be a proper 10 digit number
190 print "setCallbackNumber %s" % (callbacknumber)
192 callbackPostData = urllib.urlencode({'a_t' : self._accessToken, 'default_number' : callbacknumber })
193 callbackSetPage = self._browser.download(GCDialer._setforwardURL, callbackPostData)
194 self._browser.cookies.save()
198 def getCallbackNumber(self):
199 for c in self._browser.cookies:
200 if c.name == "pda_forwarding_number":
204 def get_recent(self):
206 recentCallsPage = self._browser.download(GCDialer._inboxallURL)
207 for match in self._inboxRe.finditer(recentCallsPage):
208 yield (match.group(4), "%s on %s from/to %s - %s" % (match.group(1).capitalize(), match.group(2), match.group(3), match.group(4)))
212 def _grabToken(self, data):
213 "Pull the magic cookie from the datastream"
214 atGroup = GCDialer._accessTokenRe.search(data)
216 self._accessToken = atGroup.group(1)
220 anGroup = GCDialer._accountNumRe.search(data)
222 self._accountNum = anGroup.group(1)
226 self._callbackNumbers = {}
228 for match in GCDialer._callbackRe.finditer(data):
229 self._callbackNumbers[match.group(1)] = match.group(2)