1 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2 # Lesser General Public License for more details.
4 # You should have received a copy of the GNU Lesser General Public
5 # License along with this library; if not, write to the Free Software
6 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
9 Google Voice backend code
12 http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
13 http://posttopic.com/topic/google-voice-add-on-development
24 from xml.etree import ElementTree
26 from browser_emu import MozillaEmulator
35 socket.setdefaulttimeout(5)
38 _TRUE_REGEX = re.compile("true")
39 _FALSE_REGEX = re.compile("false")
43 s = _TRUE_REGEX.sub("True", s)
44 s = _FALSE_REGEX.sub("False", s)
45 return eval(s, {}, {})
48 if simplejson is None:
49 def parse_json(flattened):
50 return safe_eval(flattened)
52 def parse_json(flattened):
53 return simplejson.loads(json)
56 class GVDialer(object):
58 This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
59 the functions include login, setting up a callback number, and initalting a callback
62 _contactsRe = re.compile(r"""<a href="/mobile/contacts/detail/(\d+)">(.*?)</a>""", re.S)
63 _contactsNextRe = re.compile(r""".*<a href="/mobile/contacts(\?page=\d+)">Next</a>""", re.S)
64 _contactDetailGroupRe = re.compile(r"""Group:\s*(\w*)""", re.S)
65 _contactDetailPhoneRe = re.compile(r"""(\w+):[0-9\-\(\) \t]*?<a href="/mobile/calls/click_to_call\?destno=(\d+).*?">call</a>""", re.S)
67 _isNotLoginPageRe = re.compile(r"""I cannot access my account""")
68 _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
69 _accountNumRe = re.compile(r"""<b class="ms2">(.{14})</b></div>""")
70 _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
71 _validateRe = re.compile("^[0-9]{10,}$")
72 _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
74 _clicktocallURL = "http://www.google.com/voice/m/sendcall"
75 _contactsURL = "http://www.google.com/voice/m/contacts"
76 _contactDetailURL = "http://www.google.com/voice/m/contact"
78 _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
79 _accountNumberURL = "https://www.google.com/voice/mobile"
80 _forwardURL = "https://www.google.com/voice/m/phones"
82 _inboxURL = "https://www.google.com/voice/inbox/"
83 _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
84 _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
85 _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
86 _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
88 def __init__(self, cookieFile = None):
89 # Important items in this function are the setup of the browser emulation and cookie file
90 self._browser = MozillaEmulator(None, 0)
91 if cookieFile is None:
92 cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
93 self._browser.cookies.filename = cookieFile
94 if os.path.isfile(cookieFile):
95 self._browser.cookies.load()
97 self._accountNum = None
98 self._lastAuthed = 0.0
100 self._callbackNumber = ""
101 self._callbackNumbers = {}
103 self.__contacts = None
105 def is_authed(self, force = False):
107 Attempts to detect a current session
108 @note Once logged in try not to reauth more than once a minute.
109 @returns If authenticated
112 if (time.time() - self._lastAuthed) < 60 and not force:
116 inboxPage = self._browser.download(self._inboxURL)
117 except urllib2.URLError, e:
118 raise RuntimeError("%s is not accesible" % self._inboxURL)
120 self._browser.cookies.save()
121 if self._isNotLoginPageRe.search(inboxPage) is not None:
124 self._lastAuthed = time.time()
127 def login(self, username, password):
129 Attempt to login to grandcentral
130 @returns Whether login was successful or not
132 #if self.is_authed():
135 loginPostData = urllib.urlencode({
138 'service': "grandcentral",
142 loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
143 except urllib2.URLError, e:
144 raise RuntimeError("%s is not accesible" % self._loginURL)
146 #self._grab_account_info(loginSuccessOrFailurePage)
147 self._grab_account_info()
148 return self.is_authed()
151 self._lastAuthed = 0.0
152 self._browser.cookies.clear()
153 self._browser.cookies.save()
157 def dial(self, number):
159 This is the main function responsible for initating the callback
161 if not self.is_valid_syntax(number):
162 raise ValueError('Number is not valid: "%s"' % number)
163 elif not self.is_authed():
164 raise RuntimeError("Not Authenticated")
166 if len(number) == 11 and number[0] == 1:
167 # Strip leading 1 from 11 digit dialing
171 clickToCallData = urllib.urlencode({
173 "phone": self._callbackNumber,
174 "_rnr_se": self._token,
178 'Referer': 'https://www.google.com/voice/m/callsms',
180 callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, otherData)
181 except urllib2.URLError, e:
182 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
184 if self._gvDialingStrRe.search(callSuccessPage) is None:
185 raise RuntimeError("Grand Central returned an error")
189 def clear_caches(self):
190 self.__contacts = None
192 def is_valid_syntax(self, number):
194 @returns If This number be called ( syntax validation only )
196 return self._validateRe.match(number) is not None
198 def get_account_number(self):
200 @returns The grand central phone number
202 return self._accountNum
204 def set_sane_callback(self):
206 Try to set a sane default callback number on these preferences
207 1) 1747 numbers ( Gizmo )
208 2) anything with gizmo in the name
209 3) anything with computer in the name
212 numbers = self.get_callback_numbers()
214 for number, description in numbers.iteritems():
215 if not re.compile(r"""1747""").match(number) is None:
216 self.set_callback_number(number)
219 for number, description in numbers.iteritems():
220 if not re.compile(r"""gizmo""", re.I).search(description) is None:
221 self.set_callback_number(number)
224 for number, description in numbers.iteritems():
225 if not re.compile(r"""computer""", re.I).search(description) is None:
226 self.set_callback_number(number)
229 for number, description in numbers.iteritems():
230 self.set_callback_number(number)
233 def get_callback_numbers(self):
235 @returns a dictionary mapping call back numbers to descriptions
236 @note These results are cached for 30 minutes.
238 if time.time() - self._lastAuthed < 1800 or self.is_authed():
239 return self._callbackNumbers
243 def set_callback_number(self, callbacknumber):
245 Set the number that grandcental calls
246 @param callbacknumber should be a proper 10 digit number
248 self._callbackNumber = callbacknumber
251 def get_callback_number(self):
253 @returns Current callback number or None
255 return self._callbackNumber
257 def get_recent(self):
259 @returns Iterable of (personsName, phoneNumber, date, action)
262 self._receivedCallsURL,
263 self._missedCallsURL,
264 self._placedCallsURL,
267 allRecentData = self._grab_json(url)
268 except urllib2.URLError, e:
269 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
271 for recentCallData in allRecentData["messages"].itervalues():
272 number = recentCallData["displayNumber"]
273 date = recentCallData["relativeStartTime"]
276 for label in recentCallData["labels"]
277 if label.lower() != "all" and label.lower() != "inbox"
279 yield "", number, date, action
281 def get_addressbooks(self):
283 @returns Iterable of (Address Book Factory, Book Id, Book Name)
287 def open_addressbook(self, bookId):
291 def contact_source_short_name(contactId):
296 return "Google Voice"
298 def get_contacts(self):
300 @returns Iterable of (contact id, contact name)
302 if self.__contacts is None:
305 contactsPagesUrls = [self._contactsURL]
306 for contactsPageUrl in contactsPagesUrls:
308 contactsPage = self._browser.download(contactsPageUrl)
309 except urllib2.URLError, e:
310 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
311 for contact_match in self._contactsRe.finditer(contactsPage):
312 contactId = contact_match.group(1)
313 contactName = contact_match.group(2)
314 contact = contactId, contactName
315 self.__contacts.append(contact)
318 next_match = self._contactsNextRe.match(contactsPage)
319 if next_match is not None:
320 newContactsPageUrl = self._contactsURL + next_match.group(1)
321 contactsPagesUrls.append(newContactsPageUrl)
323 for contact in self.__contacts:
326 def get_contact_details(self, contactId):
328 @returns Iterable of (Phone Type, Phone Number)
331 detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
332 except urllib2.URLError, e:
333 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
335 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
336 phoneType = detail_match.group(1)
337 phoneNumber = detail_match.group(2)
338 yield (phoneType, phoneNumber)
340 def _grab_json(self, url):
341 flatXml = self._browser.download(url)
342 xmlTree = ElementTree.fromstring(flatXml)
343 jsonElement = xmlTree.getchildren()[0]
344 flatJson = jsonElement.text
345 jsonTree = parse_json(flatJson)
348 def _grab_account_info(self, loginPage = None):
349 if loginPage is None:
350 accountNumberPage = self._browser.download(self._accountNumberURL)
352 accountNumberPage = loginPage
353 tokenGroup = self._tokenRe.search(accountNumberPage)
354 if tokenGroup is not None:
355 self._token = tokenGroup.group(1)
356 anGroup = self._accountNumRe.search(accountNumberPage)
357 if anGroup is not None:
358 self._accountNum = anGroup.group(1)
360 callbackPage = self._browser.download(self._forwardURL)
361 self._callbackNumbers = {}
362 for match in self._callbackRe.finditer(callbackPage):
363 self._callbackNumbers[match.group(2)] = match.group(1)
364 if len(self._callbackNumber) == 0:
365 self.set_sane_callback()
368 def test_backend(username, password):
371 print "Authenticated: ", backend.is_authed()
372 print "Login?: ", backend.login(username, password)
373 print "Authenticated: ", backend.is_authed()
374 print "Token: ", backend._token
375 print "Account: ", backend.get_account_number()
376 print "Callback: ", backend.get_callback_number()
377 print "All Callback: ",
378 pprint.pprint(backend.get_callback_numbers())
380 pprint.pprint(list(backend.get_recent()))