4 Grandcentral Dialer backend code
5 Eric Warnke <ericew@gmail.com>
17 from browser_emu import MozillaEmulator
20 class GCDialer(object):
22 This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
23 the functions include login, setting up a callback number, and initalting a callback
26 _gcDialingStrRe = re.compile("This may take a few seconds", re.M)
27 _accessTokenRe = re.compile(r"""<input type="hidden" name="a_t" [^>]*value="(.*)"/>""")
28 _isLoginPageRe = re.compile(r"""<form method="post" action="https://www.grandcentral.com/mobile/account/login">""")
29 _callbackRe = re.compile(r"""name="default_number" value="(\d+)" />\s+(.*)\s$""", re.M)
30 _accountNumRe = re.compile(r"""<img src="/images/mobile/inbox_logo.gif" alt="GrandCentral" />\s*(.{14})\s* """, re.M)
31 _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 _contactsRe = re.compile(r"""<a href="/mobile/contacts/detail/(\d+)">(.*?)</a>""", re.S)
33 _contactsNextRe = re.compile(r""".*<a href="/mobile/contacts(\?page=\d+)">Next</a>""", re.S)
34 _contactDetailGroupRe = re.compile(r"""Group:\s*(\w*)""", re.S)
35 _contactDetailPhoneRe = re.compile(r"""(\w+):[0-9\-\(\) \t]*?<a href="/mobile/calls/click_to_call\?destno=(\d+).*?">call</a>""", re.S)
37 _validateRe = re.compile("^[0-9]{10,}$")
39 _forwardselectURL = "http://www.grandcentral.com/mobile/settings/forwarding_select"
40 _loginURL = "https://www.grandcentral.com/mobile/account/login"
41 _setforwardURL = "http://www.grandcentral.com/mobile/settings/set_forwarding?from=settings"
42 _clicktocallURL = "http://www.grandcentral.com/mobile/calls/click_to_call?a_t=%s&destno=%s"
43 _inboxallURL = "http://www.grandcentral.com/mobile/messages/inbox?types=all"
44 _contactsURL = "http://www.grandcentral.com/mobile/contacts"
45 _contactDetailURL = "http://www.grandcentral.com/mobile/contacts/detail"
47 def __init__(self, cookieFile = None):
48 # Important items in this function are the setup of the browser emulation and cookie file
51 self._browser = MozillaEmulator(None, 0)
52 if cookieFile is None:
53 cookieFile = os.path.join(os.path.expanduser("~"), ".gc_dialer_cookies.txt")
54 self._browser.cookies.filename = cookieFile
55 if os.path.isfile(cookieFile):
56 self._browser.cookies.load()
58 self._accessToken = None
59 self._accountNum = None
60 self._callbackNumbers = {}
61 self._lastAuthed = 0.0
63 def is_authed(self, force = False):
65 Attempts to detect a current session and pull the auth token ( a_t ) from the page.
66 @note Once logged in try not to reauth more than once a minute.
67 @returns If authenticated
70 if time.time() - self._lastAuthed < 60 and not force:
74 forwardSelectionPage = self._browser.download(GCDialer._forwardselectURL)
75 except urllib2.URLError, e:
76 warnings.warn("%s is not accesible" % GCDialer._forwardselectURL, UserWarning, 2)
79 self._browser.cookies.save()
80 if GCDialer._isLoginPageRe.search(forwardSelectionPage) is None:
81 self._grab_token(forwardSelectionPage)
82 self._lastAuthed = time.time()
87 def login(self, username, password):
89 Attempt to login to grandcentral
90 @returns Whether login was successful or not
95 loginPostData = urllib.urlencode( {'username' : username , 'password' : password } )
98 loginSuccessOrFailurePage = self._browser.download(GCDialer._loginURL, loginPostData)
99 except urllib2.URLError, e:
100 warnings.warn("%s is not accesible" % GCDialer._loginURL, UserWarning, 2)
103 return self.is_authed()
106 self._lastAuthed = 0.0
107 self._browser.cookies.clear()
108 self._browser.cookies.save()
110 def dial(self, number):
112 This is the main function responsible for initating the callback
116 # If the number is not valid throw exception
117 if not self.is_valid_syntax(number):
118 raise ValueError('number is not valid')
120 # No point if we don't have the magic cookie
121 if not self.is_authed():
122 self._msg = "Not authenticated"
125 # Strip leading 1 from 11 digit dialing
126 if len(number) == 11 and number[0] == 1:
130 callSuccessPage = self._browser.download(
131 GCDialer._clicktocallURL % (self._accessToken, number),
133 {'Referer' : 'http://www.grandcentral.com/mobile/messages'}
135 except urllib2.URLError, e:
136 warnings.warn("%s is not accesible" % GCDialer._clicktocallURL, UserWarning, 2)
139 if GCDialer._gcDialingStrRe.search(callSuccessPage) is not None:
142 self._msg = "Grand Central returned an error"
145 self._msg = "Unknown Error"
148 def clear_caches(self):
151 def is_valid_syntax(self, number):
153 @returns If This number be called ( syntax validation only )
155 return self._validateRe.match(number) is not None
157 def get_account_number(self):
159 @returns The grand central phone number
161 return self._accountNum
163 def set_sane_callback(self):
165 Try to set a sane default callback number on these preferences
166 1) 1747 numbers ( Gizmo )
167 2) anything with gizmo in the name
168 3) anything with computer in the name
171 numbers = self.get_callback_numbers()
173 for number, description in numbers.iteritems():
174 if not re.compile(r"""1747""").match(number) is None:
175 self.set_callback_number(number)
178 for number, description in numbers.iteritems():
179 if not re.compile(r"""gizmo""", re.I).search(description) is None:
180 self.set_callback_number(number)
183 for number, description in numbers.iteritems():
184 if not re.compile(r"""computer""", re.I).search(description) is None:
185 self.set_callback_number(number)
188 for number, description in numbers.iteritems():
189 self.set_callback_number(number)
192 def get_callback_numbers(self):
194 @returns a dictionary mapping call back numbers to descriptions
195 @note These results are cached for 30 minutes.
197 if time.time() - self._lastAuthed < 1800 or self.is_authed():
198 return self._callbackNumbers
202 def set_callback_number(self, callbacknumber):
204 Set the number that grandcental calls
205 @param callbacknumber should be a proper 10 digit number
207 callbackPostData = urllib.urlencode({
208 'a_t': self._accessToken,
209 'default_number': callbacknumber
212 callbackSetPage = self._browser.download(GCDialer._setforwardURL, callbackPostData)
213 except urllib2.URLError, e:
214 warnings.warn("%s is not accesible" % GCDialer._setforwardURL, UserWarning, 2)
217 self._browser.cookies.save()
220 def get_callback_number(self):
222 @returns Current callback number or None
224 for c in self._browser.cookies:
225 if c.name == "pda_forwarding_number":
229 def get_recent(self):
231 @returns Iterable of (personsName, phoneNumber, date, action)
234 recentCallsPage = self._browser.download(GCDialer._inboxallURL)
235 except urllib2.URLError, e:
236 warnings.warn("%s is not accesible" % GCDialer._inboxallURL, UserWarning, 2)
239 for match in self._inboxRe.finditer(recentCallsPage):
240 phoneNumber = match.group(4)
241 action = match.group(1)
242 date = match.group(2)
243 personsName = match.group(3)
244 yield personsName, phoneNumber, date, action
246 def get_contacts(self):
248 @returns Iterable of (contact id, contact name)
250 contactsPagesUrls = [GCDialer._contactsURL]
251 for contactsPageUrl in contactsPagesUrls:
252 contactsPage = self._browser.download(contactsPageUrl)
253 for contact_match in self._contactsRe.finditer(contactsPage):
254 contactId = contact_match.group(1)
255 contactName = contact_match.group(2)
256 yield contactId, contactName
258 next_match = self._contactsNextRe.match(contactsPage)
259 if next_match is not None:
260 newContactsPageUrl = self._contactsURL + next_match.group(1)
261 contactsPagesUrls.append(newContactsPageUrl)
263 def get_contact_details(self, contactId):
265 @returns Iterable of (Phone Type, Phone Number)
267 detailPage = self._browser.download(GCDialer._contactDetailURL + '/' + contactId)
268 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
269 phoneType = detail_match.group(1)
270 phoneNumber = detail_match.group(2)
271 yield (phoneType, phoneNumber)
273 def _grab_token(self, data):
274 "Pull the magic cookie from the datastream"
275 atGroup = GCDialer._accessTokenRe.search(data)
276 self._accessToken = atGroup.group(1)
278 anGroup = GCDialer._accountNumRe.search(data)
279 self._accountNum = anGroup.group(1)
281 self._callbackNumbers = {}
282 for match in GCDialer._callbackRe.finditer(data):
283 self._callbackNumbers[match.group(1)] = match.group(2)