Google Voice and Asterisk

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:

  1. You pick up your SIP phone and make a call.
  2. Asterisk receives your call and the google-voice-dialout.agi script is called.
  3. The script contacts Google Voice and initiates a call based on the number passed to asterisk.
  4. Google Voice then dials your Gizmo5 number which rings into Asterisk.
  5. Asterisk identifies this call by its caller id and the script binds your outbound call and the incoming call from Google
  6. Google proceeds to call out to your destination.
  7. Call is connected!

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()

 

Leave a Reply