Google Voice Dialing Script for Asterisk

derekneuberger

New Member
Joined
Aug 4, 2008
Messages
14
Reaction score
0
I was planning on posting this once I got it working, but I have been unable to get fully there. :( So, I figured I would post it now to tap into some the knowledge floating around here. Any help on this is *greatly* appreciated!

I also want everyone to know that the credit for this script goes to its respective author, Paul Marks of http://www.pcmarks.net . I understand the script works fine in a roll-your-own Asterisk 1.6 installation. I am just having trouble implementing it on a build with Freepbx (PIAF, in my case). Last but not least, I was made aware of this script by kmem in the #pbxinaflash channel on freenode. Some of the code I will post is his or came from him and all of the credit of finding this gem goes to him.

Without further ado:
Code:
#!/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
#

# 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.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()
Code is also attached (change filename ending from txt to agi) and available at original source: http://www.pmarks.net/posted_links/google-voice-dialout.agi .

This is where I am at:

I copied the script to "/var/lib/asterisk/agi-bin" and made it executable by issuing "chmod -x google-voice-dialout.agi" . I already have an inbound route setup for my gizmo account, so running the script manually (like instructed in the notes) successfully completes a call. However, when trying to complete an outbound call from an endpoint it fails. I have tracked what I believe to be the problem as the "agi_dnid" not being pulled properly (it is being delivered to the script as "unknown"). Further, I believe this problem is being caused by an improper outbound route on my part. My outbound route setup is as follows:

I have an outbound route pointing to a custom trunk. The custom trunk is:
Code:
Custom Dial String: Local/$OUTNUM$@gv-doout-custom
I have the custom context, "gv-doout-custom", added to my "extensions_custom.conf" file:

Code:
[gv-doout-custom]
exten => _1NXXNXXXXXX,1,AGI(google-voice-dialout.agi)
I haven't gotten to the perils of ensuring the bridge statement in the inbound route functions. I am making the assumption that if someone points me down the right path with the outbound, the inbound might fall into place.

Thank you again for any help provided and looking forward to getting Google calling through PIAF a reality again!

~derek
 

Attachments

  • google-voice-dialout.txt
    5.5 KB · Views: 1

dswartz

Guru
Joined
Feb 17, 2009
Messages
1,056
Reaction score
0
Ugh, I can't help but wonder why someone felt compelled to write this in python, when we already have a ton of stuff written in php - now there are two scripting languages to keep track of (actually 3, if you count perl.)
 

dswartz

Guru
Joined
Feb 17, 2009
Messages
1,056
Reaction score
0
Biased here, but I must admit I far prefer PHP, since it is very "C" like (a language I am extremely familiar with.) We won't even go into how badly perl sucks :)
 

derekneuberger

New Member
Joined
Aug 4, 2008
Messages
14
Reaction score
0
Ok, The bridge line does work in 1.6, but obviously is a no go in 1.4. After discussing this a little on the IRC channel, I think the best way to join the calls (the incoming google voice via gizmo with your ext to server) is with the meetme. I am at a loss on how to actually get the implemented in the dialplan. Something like:
Code:
exten => s,1,MeetMe(|Mde)
ought to dump into a dynamic conference, no pin needed. I tested this out in my [from-gizmo] context. You would, obviously, need some error handling to only route the appropriate calls from your gizmo trunk in this fashion..

I am also thinking that channelredirect can be used to grab your active ext>server call and dump it in the conference to join the call from gizmo just routed there. http://astbook.asteriskdocs.org/en/2nd_Edition/asterisk-book-html-chunk/asterisk-APP-B-349.html
 

derekneuberger

New Member
Joined
Aug 4, 2008
Messages
14
Reaction score
0
I didn't quite complete my thought there. The problem I am running into, is somehow using the dialplan to initiate the channelredirect line to join the calls after the gizmo call comes in.

Also, some ideas on the error handling of the gizmo calls:
Code:
exten => s,1,GotoIf($[${CALLERID(num)} = GVNUMBER]?reject:allow) ;matches with your gvnumber. if only your gizmo route was using this context, this would work, otherwise, you would also need to patternmatch on the ext.
exten => s,n(allow),Set(CALLERID(all)=${CALLERID(num)} <${CALLERID(num)}>)
exten => s,n,Dial(sip/EXT) ; or whereever your plan B would send it
exten => s,n,Hangup()
exten => s,n(reject),Bridge(${DB_DELETE(gv_dialout/channel)}, p)
exten => s,n,Hangup()
 

wardmundy

Nerd Uno
Joined
Oct 12, 2007
Messages
19,201
Reaction score
5,221
I must be missing something here. For inbound calls, what's wrong with setting up a forwarding number on GV and then using that DID to accept calls on your Asterisk box?? Then you don't need the bridging at all. :confused5:
 

derekneuberger

New Member
Joined
Aug 4, 2008
Messages
14
Reaction score
0
Ward,

The idea of the bridge is so it appears and acts like a regular outbound route. So:
a) I pick up my phone and dial the number
b) #behind the scenes asterisk initiates the click2call, excepts the incoming call from GV and bridges (or for 1.4's case would have to dump both calls into the same conference).
c) I am presented with my called parties phone ringing without ever having to pickup another call

Now, given, unless you were calling in from the outside or from an extensions that wouldn't normally receive inbound calls, most of that side of the script is luxury. However, it would be pretty awesome!

~derek
 

wardmundy

Nerd Uno
Joined
Oct 12, 2007
Messages
19,201
Reaction score
5,221
Ah. Got it now. With Call Forward to same extension I dialed from, I have to punch the answer button a second time on a 57i, but I understand the coolness factor. :blush2: For many folks, they might actually prefer to redirect the conversation to their cell. I think I'll leave that for another day and cover the basics on Monday.
 

zipeee

Member
Joined
Jun 15, 2009
Messages
85
Reaction score
0
So on Asterisk 1.6, what would be the proper way to bridge the dial-out channel and the incoming GV did? Any easy way to implement this?
 

Members online

No members online now.

Forum statistics

Threads
25,812
Messages
167,763
Members
19,241
Latest member
bellabos
Get 3CX - Absolutely Free!

Link up your team and customers Phone System Live Chat Video Conferencing

Hosted or Self-managed. Up to 10 users free forever. No credit card. Try risk free.

3CX
A 3CX Account with that email already exists. You will be redirected to the Customer Portal to sign in or reset your password if you've forgotten it.
Top