Many months back Paul Marks and I looked into how to integrate Google Voice and Asterisk. Inbound calling was simple as you direct your Google Voice account to a Gizmo5 number and Asterisk integrates with the open SIP standard that Gizmo5 uses.
However, our task was to be able to make outbound calls without the need to use the web interface to place the call. What essentially had to happen is when you dialed a number on your SIP phone, Asterisk would have to talk to the Google Voice site and handle the call setup for you.
***UPDATE***: I worked with Ward Mundy of Nerd Vittles who put togeather a complete solution for both Asterisk 1.4 and 1.6. Additionally we are now using pyGoogleVoice which is a more complete library than Paul’s script below. For the full write up, check out: http://nerdvittles.com/?p=635
Paul then created a Python AGI (Asterisk Gateway Interface) script. It integrates into your dial plan and allows for the call to be made. The logic flow is as follows:
Now a few caveats to this approach. As Paul’s script is essentially screen scraping using unpublished API’s to the Google Voice application, any changes made to their web API could break this script. (As he notes in the script comments) At any rate, at the time of writing it works and the script is available for download here, google-voice-dialout.agi, or shown below for reference.
When I get some more time, I will post details how to integrate this into an Asterisk dialplan and other features.
#!/usr/bin/env python # google-voice-dialout.agi # Paul Marks (http://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 # # Put your Google login and Gizmo number here: USERNAME = "username@gmail.com" PASSWORD = "password" GIZMO_NUMBER = "17475555555" 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.1" 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, "Logging in." postdata = urllib.urlencode({ "Email": self.username, "Passwd": self.password }) 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, "_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(10) finally: SendAgi("hangup") if __name__ == '__main__': main()