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 _isNotLoginPageRe = re.compile(r"""I cannot access my account""")
63 _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
64 _accountNumRe = re.compile(r"""<b class="ms2">(.{14})</b></div>""")
65 _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
66 _validateRe = re.compile("^[0-9]{10,}$")
67 _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
69 _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
70 _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
71 _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
73 _clicktocallURL = "https://www.google.com/voice/m/sendcall"
74 _contactsURL = "https://www.google.com/voice/mobile/contacts"
75 _contactDetailURL = "https://www.google.com/voice/mobile/contact"
77 _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
78 _setforwardURL = "https://www.google.com//voice/m/setphone"
79 _accountNumberURL = "https://www.google.com/voice/mobile"
80 _forwardURL = "https://www.google.com/voice/mobile/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,
177 'Referer' : 'https://google.com/voice/m/callsms',
179 callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
180 except urllib2.URLError, e:
181 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
183 if self._gvDialingStrRe.search(callSuccessPage) is None:
184 raise RuntimeError("Google Voice returned an error")
188 def clear_caches(self):
189 self.__contacts = None
191 def is_valid_syntax(self, number):
193 @returns If This number be called ( syntax validation only )
195 return self._validateRe.match(number) is not None
197 def get_account_number(self):
199 @returns The grand central phone number
201 return self._accountNum
203 def set_sane_callback(self):
205 Try to set a sane default callback number on these preferences
206 1) 1747 numbers ( Gizmo )
207 2) anything with gizmo in the name
208 3) anything with computer in the name
211 numbers = self.get_callback_numbers()
213 for number, description in numbers.iteritems():
214 if re.compile(r"""1747""").match(number) is not None:
215 self.set_callback_number(number)
218 for number, description in numbers.iteritems():
219 if re.compile(r"""gizmo""", re.I).search(description) is not None:
220 self.set_callback_number(number)
223 for number, description in numbers.iteritems():
224 if re.compile(r"""computer""", re.I).search(description) is not None:
225 self.set_callback_number(number)
228 for number, description in numbers.iteritems():
229 self.set_callback_number(number)
232 def get_callback_numbers(self):
234 @returns a dictionary mapping call back numbers to descriptions
235 @note These results are cached for 30 minutes.
237 if time.time() - self._lastAuthed < 1800 or self.is_authed():
238 return self._callbackNumbers
242 def set_callback_number(self, callbacknumber):
244 Set the number that grandcental calls
245 @param callbacknumber should be a proper 10 digit number
247 self._callbackNumber = callbacknumber
248 callbackPostData = urllib.urlencode({
249 '_rnr_se': self._token,
250 'phone': callbacknumber
253 callbackSetPage = self._browser.download(self._setforwardURL, callbackPostData)
254 except urllib2.URLError, e:
255 raise RuntimeError("%s is not accesible" % self._setforwardURL)
257 self._browser.cookies.save()
260 def get_callback_number(self):
262 @returns Current callback number or None
264 return self._callbackNumber
266 def get_recent(self):
268 @returns Iterable of (personsName, phoneNumber, date, action)
271 self._receivedCallsURL,
272 self._missedCallsURL,
273 self._placedCallsURL,
276 allRecentData = self._grab_json(url)
277 except urllib2.URLError, e:
278 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
280 for recentCallData in allRecentData["messages"].itervalues():
281 number = recentCallData["displayNumber"]
282 date = recentCallData["relativeStartTime"]
285 for label in recentCallData["labels"]
286 if label.lower() != "all" and label.lower() != "inbox"
288 yield "", number, date, action
290 def get_addressbooks(self):
292 @returns Iterable of (Address Book Factory, Book Id, Book Name)
296 def open_addressbook(self, bookId):
300 def contact_source_short_name(contactId):
305 return "Google Voice"
307 def get_contacts(self):
309 @returns Iterable of (contact id, contact name)
311 if self.__contacts is None:
314 contactsPagesUrls = [self._contactsURL]
315 for contactsPageUrl in contactsPagesUrls:
317 contactsPage = self._browser.download(contactsPageUrl)
318 except urllib2.URLError, e:
319 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
320 for contact_match in self._contactsRe.finditer(contactsPage):
321 contactId = contact_match.group(1)
322 contactName = contact_match.group(2)
323 contact = contactId, contactName
324 self.__contacts.append(contact)
327 next_match = self._contactsNextRe.match(contactsPage)
328 if next_match is not None:
329 newContactsPageUrl = self._contactsURL + next_match.group(1)
330 contactsPagesUrls.append(newContactsPageUrl)
332 for contact in self.__contacts:
335 def get_contact_details(self, contactId):
337 @returns Iterable of (Phone Type, Phone Number)
340 detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
341 except urllib2.URLError, e:
342 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
344 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
345 phoneNumber = detail_match.group(1)
346 phoneType = detail_match.group(2)
347 yield (phoneType, phoneNumber)
349 def _grab_json(self, url):
350 flatXml = self._browser.download(url)
351 xmlTree = ElementTree.fromstring(flatXml)
352 jsonElement = xmlTree.getchildren()[0]
353 flatJson = jsonElement.text
354 jsonTree = parse_json(flatJson)
357 def _grab_account_info(self, loginPage = None):
358 if loginPage is None:
359 accountNumberPage = self._browser.download(self._accountNumberURL)
361 accountNumberPage = loginPage
362 tokenGroup = self._tokenRe.search(accountNumberPage)
363 if tokenGroup is not None:
364 self._token = tokenGroup.group(1)
365 anGroup = self._accountNumRe.search(accountNumberPage)
366 if anGroup is not None:
367 self._accountNum = anGroup.group(1)
369 callbackPage = self._browser.download(self._forwardURL)
370 self._callbackNumbers = {}
371 for match in self._callbackRe.finditer(callbackPage):
372 self._callbackNumbers[match.group(2)] = match.group(1)
373 if len(self._callbackNumber) == 0:
374 self.set_sane_callback()
377 def test_backend(username, password):
380 print "Authenticated: ", backend.is_authed()
381 print "Login?: ", backend.login(username, password)
382 print "Authenticated: ", backend.is_authed()
383 print "Token: ", backend._token
384 print "Account: ", backend.get_account_number()
385 print "Callback: ", backend.get_callback_number()
386 # print "All Callback: ",
387 # pprint.pprint(backend.get_callback_numbers())
389 # pprint.pprint(list(backend.get_recent()))
390 # print "Contacts: ",
391 # for contact in backend.get_contacts():
393 # pprint.pprint(list(backend.get_contact_details(contact[0])))