Reply
 
Thread Tools Display Modes
  #1  
Old 07-23-09, 11:16 AM
derekneuberger derekneuberger is offline
Junior Member
 
Join Date: Aug 2008
Posts: 14
Google Voice Dialing Script for Asterisk
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/g...ce-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
Attached Files:
File Type: txt google-voice-dialout.txt (5.5 KB, 1 views)

Last edited by derekneuberger : 07-24-09 at 02:42 AM.
Reply With Quote
  #2  
Old 07-24-09, 01:17 PM
wardmundy wardmundy is offline
Nerd Uno
 
Join Date: Oct 2007
Posts: 3,881
Finally got this working. We'll write up the solution this weekend and publish early Monday on Nerd Vittles at the following link: http://nerdvittles.com/?p=630
Reply With Quote
  #3  
Old 07-24-09, 01:45 PM
dswartz dswartz is offline
Guru
 
Join Date: Feb 2009
Posts: 575
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.)
Reply With Quote
  #4  
Old 07-24-09, 02:53 PM
wardmundy wardmundy is offline
Nerd Uno
 
Join Date: Oct 2007
Posts: 3,881
You got that right. If it weren't the most requested article on Nerd Vittles all day every day, we'd have run away from it without ever looking back.

Last edited by wardmundy : 07-24-09 at 03:43 PM.
Reply With Quote
  #5  
Old 07-24-09, 03:04 PM
dswartz dswartz is offline
Guru
 
Join Date: Feb 2009
Posts: 575
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
Reply With Quote
  #6  
Old 07-24-09, 06:25 PM
derekneuberger derekneuberger is offline
Junior Member
 
Join Date: Aug 2008
Posts: 14
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/2...APP-B-349.html
Reply With Quote
  #7  
Old 07-24-09, 06:30 PM
derekneuberger derekneuberger is offline
Junior Member
 
Join Date: Aug 2008
Posts: 14
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()
Reply With Quote
  #8  
Old 07-24-09, 06:59 PM
wardmundy wardmundy is offline
Nerd Uno
 
Join Date: Oct 2007
Posts: 3,881
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.
Reply With Quote
  #9  
Old 07-24-09, 07:58 PM
derekneuberger derekneuberger is offline
Junior Member
 
Join Date: Aug 2008
Posts: 14
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
Reply With Quote
  #10  
Old 07-24-09, 08:06 PM
wardmundy wardmundy is offline
Nerd Uno
 
Join Date: Oct 2007
Posts: 3,881
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. 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.
Reply With Quote
Reply


Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off
Forum Jump


All times are GMT -5. The time now is 09:01 PM.


Design by Vjacheslav Trushkin, color scheme by ColorizeIt!.
Powered by vBulletin®
Copyright ©2000 - 2010, Jelsoft Enterprises Ltd.
Copyright ©2007-2008, Ward Mundy & Associates