3 # GC Dialer - Front end for Google's Grand Central service.
4 # Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
6 # This library is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public
8 # License as published by the Free Software Foundation; either
9 # version 2.1 of the License, or (at your option) any later version.
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # Lesser General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this library; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 Grandcentral Dialer backend code
32 from browser_emu import MozillaEmulator
35 class GCDialer(object):
37 This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
38 the functions include login, setting up a callback number, and initalting a callback
41 _gcDialingStrRe = re.compile("This may take a few seconds", re.M)
42 _accessTokenRe = re.compile(r"""<input type="hidden" name="a_t" [^>]*value="(.*)"/>""")
43 _isLoginPageRe = re.compile(r"""<form method="post" action="https://www.grandcentral.com/mobile/account/login">""")
44 _callbackRe = re.compile(r"""name="default_number" value="(\d+)" />\s+(.*)\s$""", re.M)
45 _accountNumRe = re.compile(r"""<img src="/images/mobile/inbox_logo.gif" alt="GrandCentral" />\s*(.{14})\s* """, re.M)
46 _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)
47 _contactsRe = re.compile(r"""<a href="/mobile/contacts/detail/(\d+)">(.*?)</a>""", re.S)
48 _contactsNextRe = re.compile(r""".*<a href="/mobile/contacts(\?page=\d+)">Next</a>""", re.S)
49 _contactDetailGroupRe = re.compile(r"""Group:\s*(\w*)""", re.S)
50 _contactDetailPhoneRe = re.compile(r"""(\w+):[0-9\-\(\) \t]*?<a href="/mobile/calls/click_to_call\?destno=(\d+).*?">call</a>""", re.S)
52 _validateRe = re.compile("^[0-9]{10,}$")
54 _forwardselectURL = "http://www.grandcentral.com/mobile/settings/forwarding_select"
55 _loginURL = "https://www.grandcentral.com/mobile/account/login"
56 _setforwardURL = "http://www.grandcentral.com/mobile/settings/set_forwarding?from=settings"
57 _clicktocallURL = "http://www.grandcentral.com/mobile/calls/click_to_call?a_t=%s&destno=%s"
58 _inboxallURL = "http://www.grandcentral.com/mobile/messages/inbox?types=all"
59 _contactsURL = "http://www.grandcentral.com/mobile/contacts"
60 _contactDetailURL = "http://www.grandcentral.com/mobile/contacts/detail"
62 def __init__(self, cookieFile = None):
63 # Important items in this function are the setup of the browser emulation and cookie file
66 self._browser = MozillaEmulator(None, 0)
67 if cookieFile is None:
68 cookieFile = os.path.join(os.path.expanduser("~"), ".gc_dialer_cookies.txt")
69 self._browser.cookies.filename = cookieFile
70 if os.path.isfile(cookieFile):
71 self._browser.cookies.load()
73 self._accessToken = None
74 self._accountNum = None
75 self._callbackNumbers = {}
76 self._lastAuthed = 0.0
78 def is_authed(self, force = False):
80 Attempts to detect a current session and pull the auth token ( a_t ) from the page.
81 @note Once logged in try not to reauth more than once a minute.
82 @returns If authenticated
85 if time.time() - self._lastAuthed < 60 and not force:
89 forwardSelectionPage = self._browser.download(GCDialer._forwardselectURL)
90 except urllib2.URLError, e:
91 warnings.warn("%s is not accesible" % GCDialer._forwardselectURL, UserWarning, 2)
94 self._browser.cookies.save()
95 if GCDialer._isLoginPageRe.search(forwardSelectionPage) is None:
96 self._grab_token(forwardSelectionPage)
97 self._lastAuthed = time.time()
102 def login(self, username, password):
104 Attempt to login to grandcentral
105 @returns Whether login was successful or not
110 loginPostData = urllib.urlencode( {'username' : username , 'password' : password } )
113 loginSuccessOrFailurePage = self._browser.download(GCDialer._loginURL, loginPostData)
114 except urllib2.URLError, e:
115 warnings.warn("%s is not accesible" % GCDialer._loginURL, UserWarning, 2)
118 return self.is_authed()
121 self._lastAuthed = 0.0
122 self._browser.cookies.clear()
123 self._browser.cookies.save()
125 def dial(self, number):
127 This is the main function responsible for initating the callback
131 # If the number is not valid throw exception
132 if not self.is_valid_syntax(number):
133 raise ValueError('number is not valid')
135 # No point if we don't have the magic cookie
136 if not self.is_authed():
137 self._msg = "Not authenticated"
140 # Strip leading 1 from 11 digit dialing
141 if len(number) == 11 and number[0] == 1:
145 callSuccessPage = self._browser.download(
146 GCDialer._clicktocallURL % (self._accessToken, number),
148 {'Referer' : 'http://www.grandcentral.com/mobile/messages'}
150 except urllib2.URLError, e:
151 warnings.warn("%s is not accesible" % GCDialer._clicktocallURL, UserWarning, 2)
154 if GCDialer._gcDialingStrRe.search(callSuccessPage) is not None:
157 self._msg = "Grand Central returned an error"
160 self._msg = "Unknown Error"
163 def clear_caches(self):
166 def is_valid_syntax(self, number):
168 @returns If This number be called ( syntax validation only )
170 return self._validateRe.match(number) is not None
172 def get_account_number(self):
174 @returns The grand central phone number
176 return self._accountNum
178 def set_sane_callback(self):
180 Try to set a sane default callback number on these preferences
181 1) 1747 numbers ( Gizmo )
182 2) anything with gizmo in the name
183 3) anything with computer in the name
186 numbers = self.get_callback_numbers()
188 for number, description in numbers.iteritems():
189 if not re.compile(r"""1747""").match(number) is None:
190 self.set_callback_number(number)
193 for number, description in numbers.iteritems():
194 if not re.compile(r"""gizmo""", re.I).search(description) is None:
195 self.set_callback_number(number)
198 for number, description in numbers.iteritems():
199 if not re.compile(r"""computer""", re.I).search(description) is None:
200 self.set_callback_number(number)
203 for number, description in numbers.iteritems():
204 self.set_callback_number(number)
207 def get_callback_numbers(self):
209 @returns a dictionary mapping call back numbers to descriptions
210 @note These results are cached for 30 minutes.
212 if time.time() - self._lastAuthed < 1800 or self.is_authed():
213 return self._callbackNumbers
217 def set_callback_number(self, callbacknumber):
219 Set the number that grandcental calls
220 @param callbacknumber should be a proper 10 digit number
222 callbackPostData = urllib.urlencode({
223 'a_t': self._accessToken,
224 'default_number': callbacknumber
227 callbackSetPage = self._browser.download(GCDialer._setforwardURL, callbackPostData)
228 except urllib2.URLError, e:
229 warnings.warn("%s is not accesible" % GCDialer._setforwardURL, UserWarning, 2)
232 self._browser.cookies.save()
235 def get_callback_number(self):
237 @returns Current callback number or None
239 for c in self._browser.cookies:
240 if c.name == "pda_forwarding_number":
244 def get_recent(self):
246 @returns Iterable of (personsName, phoneNumber, date, action)
249 recentCallsPage = self._browser.download(GCDialer._inboxallURL)
250 except urllib2.URLError, e:
251 warnings.warn("%s is not accesible" % GCDialer._inboxallURL, UserWarning, 2)
254 for match in self._inboxRe.finditer(recentCallsPage):
255 phoneNumber = match.group(4)
256 action = match.group(1)
257 date = match.group(2)
258 personsName = match.group(3)
259 yield personsName, phoneNumber, date, action
261 def get_addressbooks(self):
263 @returns Iterable of (Address Book Factory, Book Id, Book Name)
265 yield self, "", "Grand Central"
267 def open_addressbook(self, bookId):
274 def get_contacts(self):
276 @returns Iterable of (contact id, contact name)
278 contactsPagesUrls = [GCDialer._contactsURL]
279 for contactsPageUrl in contactsPagesUrls:
280 contactsPage = self._browser.download(contactsPageUrl)
281 for contact_match in self._contactsRe.finditer(contactsPage):
282 contactId = contact_match.group(1)
283 contactName = contact_match.group(2)
284 yield contactId, contactName
286 next_match = self._contactsNextRe.match(contactsPage)
287 if next_match is not None:
288 newContactsPageUrl = self._contactsURL + next_match.group(1)
289 contactsPagesUrls.append(newContactsPageUrl)
291 def get_contact_details(self, contactId):
293 @returns Iterable of (Phone Type, Phone Number)
295 detailPage = self._browser.download(GCDialer._contactDetailURL + '/' + contactId)
296 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
297 phoneType = detail_match.group(1)
298 phoneNumber = detail_match.group(2)
299 yield (phoneType, phoneNumber)
301 def _grab_token(self, data):
302 "Pull the magic cookie from the datastream"
303 atGroup = GCDialer._accessTokenRe.search(data)
304 self._accessToken = atGroup.group(1)
306 anGroup = GCDialer._accountNumRe.search(data)
307 self._accountNum = anGroup.group(1)
309 self._callbackNumbers = {}
310 for match in GCDialer._callbackRe.finditer(data):
311 self._callbackNumbers[match.group(1)] = match.group(2)