#!/usr/bin/env python # google-voice-dialout.agi # Paul Marks (www.pmarks.net) # # This is an Asterisk 1.6 script to place outgoing calls through Google Voice. # It will automatically sign into the web interface, and submit a click2call # request through your registered Gizmo number. Asterisk can then answer # the incoming call, and Bridge() it into your original outgoing call. # # I deduced the click2call sequence by using the "Live HTTP Headers" Firefox # plugin. If the website changes too much, this script will probably stop # working, so don't use it for anything too important. # # This assumes you've already configured Asterisk to receive Gizmo calls. # # # This rule will redirect outbound calls to this script: # exten => _1NXXNXXXXXX,1,AGI(google-voice-dialout.agi) # # This rule will connect the inbound GV/Gizmo calls: # exten => s/6502650000,1,Bridge(${DB_DELETE(gv_dialout/channel)}, p) # ^-- Put your 10-digit Google Voice number here. # # # To test this script from the command line without Asterisk, type the # following. Be sure to type a few linefeeds at the end: # # $ ./google-voice-dialout.agi # agi_channel: # agi_dnid: 18004664411 # # Changes: # - 2009-11-19: Added "phoneType=7" parameter. # - 2009-10-08: Added handler for "GALX" on login page. # Put your Google login and Gizmo number here: USERNAME = "bob" PASSWORD = "hunter2" GIZMO_NUMBER = "17470000000" import httplib import urllib import re import sys import time class Error(Exception): pass def ReadAgiEnvironment(): env = {} while 1: line = sys.stdin.readline().strip() if not line: break key, data = line.split(':') env[key.strip()] = data.strip() return env def SendAgi(cmd): sys.stdout.write("%s\n" % cmd) sys.stdout.flush() sys.stdin.readline() class SimpleCookieJar(object): cookie_re = re.compile(r"(?i)set-cookie: (\w+)=([^;]+).*") def __init__(self): self.cookies = {} def addCookies(self, response): for header in response.msg.headers: m = self.cookie_re.match(header) if not m: continue self.cookies[m.group(1)] = m.group(2) def get(self): return "; ".join("%s=%s" % kv for kv in self.cookies.iteritems()) class GVClickToCall(object): USER_AGENT = "google-voice-dialout.agi/1.2" def __init__(self, username, password, via, dial): self.username = username self.password = password self.via = via self.dial = dial self.cj = SimpleCookieJar() self.h = httplib.HTTPSConnection("www.google.com") self.login() self.placeCall() self.logout() def login(self): print >>sys.stderr, "Getting login page." self.doRequest( method="GET", url="/accounts/ServiceLogin", headers={}) # Value of "GALX" needs to be copied for login to work. try: galx = self.cj.cookies["GALX"] except KeyError: raise Error("Can't find GALX cookie. Script must be outdated.") print >>sys.stderr, "Logging in." postdata = urllib.urlencode({ "Email": self.username, "Passwd": self.password, "GALX": galx }) self.doRequest( method="POST", url="/accounts/ServiceLoginAuth", body=postdata, headers={ "Content-Type": "application/x-www-form-urlencoded" }) # Start at https://www.google.com/voice, and collect cookies as we # follow all the redirects. PREFIX = "https://www.google.com/" location = "/voice" for i in xrange(5): response, html = self.doRequest( method="GET", url=location, headers={}) location = response.getheader("location") if not location: # No more redirects, yay! break # All redirects should fall within the same domain. if not location.startswith(PREFIX): raise Error("Unexpected redirect: %s" % location) location = location[len(PREFIX)-1:] # Scrape magic _rnr_se value from the HTML. m = re.search(r'name="_rnr_se" type="hidden" value="([^"]+)"', html) if not m: raise Error("Can't find _rnr_se. Not logged in?") self.magic_rnr_se = m.group(1) def placeCall(self): print >>sys.stderr, "Calling %s via %s" % (self.dial, self.via) postdata = urllib.urlencode({ "outgoingNumber": self.dial, "forwardingNumber": self.via, "phoneType": "7", "_rnr_se": self.magic_rnr_se }) response, http = self.doRequest( method="POST", url="/voice/call/connect", body=postdata, headers={ "Content-Type": "application/x-www-form-urlencoded" }) print >>sys.stderr, "Dial response:", http def logout(self): self.doRequest( method="GET", url="/accounts/Logout", headers={ "Connection": "close" }) print >>sys.stderr, "Logged out." def doRequest(self, headers, **kw): headers["User-agent"] = self.USER_AGENT headers["Cookie"] = self.cj.get() self.h.request(headers=headers, **kw) response = self.h.getresponse() self.cj.addCookies(response) return response, response.read() def main(): env = ReadAgiEnvironment() print >>sys.stderr, env agi_channel = env["agi_channel"] agi_dnid = env["agi_dnid"] # Write the channel ID to Asterisk's database, so it can be accessed # by the incoming call when it arrives. SendAgi("database put gv_dialout channel %s" % agi_channel) SendAgi("answer") try: GVClickToCall(username=USERNAME, password=PASSWORD, dial=agi_dnid, via=GIZMO_NUMBER) # Asterisk should patch in the incoming call while we're asleep. time.sleep(20) finally: SendAgi("hangup") if __name__ == '__main__': main()