Flexible table-controlled dialplan, Version 2

Catalog of dial plans
MikeTelis
Posts: 1582
Joined: Wed Jul 30, 2008 6:48 am

Flexible table-controlled dialplan, Version 2

Post by MikeTelis » Wed Jul 14, 2010 4:06 am

This is a new version of my Flexible table-controlled dialplan, optimized for US, GV. Code is below, "User's manual" (a sort of) is in the next post.

Code: Select all

# Copyright 2010 Mike Telis
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

require 'mikesgem'

# ************* C O N F I G U R A T I O N    S E C T I O N *************** #

Area = '408'          # my area code, this will be added to 7-digit dialouts
Tz   = -8             # my time zone (GMT format, e.g. Eastern = -5, Central = -6)

# Speed dial entries. Format: "key" => "number"

Speeddial = {
  '0'   => '370-7070',                 # Home
  '1'   => '*#2 454-1234',             # San Jose Voice Mail, dial my SJ GV using SFO GV acnt
  '2'   => '*#1 (415) 335-1234',       # San Francisco Voice Mail, dial my SFO GV using SJ GV acnt
  '3'   => '*#1 (773) 778-1234',       # San Francisco Voice Mail, dial my Chicago GV using SJ GV acnt
  '411' => '(800) 466-4411',           # Google's Directory Assistance, GOOG-411
  '303' => '303@sip.blueface.ie',      # Blueface speaking clock (Ireland time)
  '266' => '4153767253@podlinez.net',  # CNN Headlines (266 = "CNN")
  '677' => '8186882773@podlinez.net',  # NPR's most e-mailed stories (677 ="NPR")
  '742' => '6506441934@podlinez.net',  # Prairie Home Companion's, or PHC's
                                       # News from Lake Wobegon (742 = "PHC")
  '932' => '7755333366',               # Columbus OH-based national weather (932 = "WEA[ther]")
}

# CNAM table: number in ENUM format => caller's name

CNAM = {
 '(212) 555-1212' => 'Dear mom',
 '(215) 333-2211' => 'Bratty kid',
}

# login/password to Google account for contacts lookup. Comment out if you don't need this feature

GoogleContacts = {
  :login => 'myname@gmail.com',
  :pswd  => 'mypassword'
}

# Uncomment next line and insert your White Pages API key, if you have it
# WP_key = 'Insert your White Pages key here'     # White Pages API key

# Uncomment line below to enable misdialing safeguards
# EnableSafeguards = 1

# Excluded Prefixes. Provides a safeguard against accidentally calling premium numbers

ExcludedPrefixes = [
   ' 1 (900 | 809)',               # USA Premium
   ' 1 \d\d\d 555 1212',           # USA Directory assistance
   '44 (9 | 55 | 70 | 84 | 87)',   # UK Premium
   '49 (1 [^567] | 900)',          # Germany Premium
   '39 (1 | 84 | 89)',             # Italy Premium
   '420 90',                       # Czech Premium
   '32 (70 | 90\d)',               # Belgium Premium
]

# Yet another safeguard, list of blessed country codes

Allowed_Country = %w{
1 33 36 37[0-2] 380 39 41 420 44 49 61 7 86 883 886 90 972 998
}

# My own ENUM database

MyENUM = {
 '+1 (408) 334-1234' => 'brother@local', # Brother George
}

# Enum database list

EnumDB = [
  MyENUM,                             # look in MyENUM first
 'e164.org',
 'e164.info',
 'e164.arpa',
 'e164.televolution.net',
 'enum.org',
]

# Serviced domains, must be in lowercase!

Domains  = ['sipsorcery.com','sip.sipsorcery.com','sip1.sipsorcery.com','sip2.sipsorcery.com','69.59.142.213']
Host     = 'sipsorcery.com'        # Replaces "host" on incoming calls

# Google Voice accounts

Myacnt1  = { :usr => 'myname1', :pwd => 'password1' }
Myacnt2  = { :usr => 'myname2', :pwd => 'password2' }

SJaccount = [
  Myacnt1 + { :cb => '(206) 424-1234' },
  Myacnt1 + { :cb => '(253) 753-4321' },
  Myacnt1 + { :cb => '(401) 648-1234' },
]

SFaccount = [
  Myacnt2 + { :cb => '(603) 413-1234' },
  Myacnt2 + { :cb => '(747) 493-4321' },
]

# SIP accounts

F9default = VSP.new '#0', '00 ${EXTEN}@F9',      'Future-nine default route'
F9grey    = VSP.new '#2', '02 ${EXTEN}@F9',      'Future-nine grey route'
F9white   = VSP.new '#3', '03 ${EXTEN}@F9',      'Future-nine white route'
F9premium = VSP.new '#4', '04 ${EXTEN}@F9',      'Future-nine premium route'
Voxalot   = VSP.new '#8', '   ${EXTEN}@Voxalot', 'Voxalot'
Ribbit    = VSP.new '#9', '   ${EXTEN}@Ribbit',  'Ribbit'
INUM      = VSP.new  nil, '011${EXTEN}@Gizmo',   'Gizmo'

SanJose   = GV.new  '*#1', nil, 'GV-408', :account => SJaccount,  :repeat => 3, :rand => true
Frisco    = GV.new  '*#2', nil, 'GV-415', :account => SFaccount,  :repeat => 2
Chicago   = GV.new  '*#3', nil, 'GV-773', :usr => 'myname3', :pwd => 'password3',
                                          :cb => '(305) 760-1234', :repeat => 2

            VSP.new '**', 'DisableSafeGuards'     # Disable safeguards prefix is **

# ********************  s e l e c t   V S P  *******************************

def selectVSP    # VoIP provider selection

  case @num
    when /^\*/                                       # For *500, *600 and other Voxalot services
      route_to Voxalot, "Voxalot Services", nil

    when /^883/                                      # iNUM
      route_to INUM, "iNUM", nil                     # disable ENUM

    when /(^1([2-9]\d\d)[2-9]\d{6})/                 # North America
      @num = $1                                      # Truncate to 11 digits
      case $2                                        # check area code
        when "800", "866", "877", "888"              # toll free numbers...
          route_to SanJose, "USA toll-free", nil     # call from GV/San Jose, disable ENUM search
        when "415"                                   # San Francisco patch, I have GV number there
          route_to Frisco, "San Francisco"
        when "312", "773", "872"                     # Chicago, I have GV number there
          route_to Chicago, "Chicago"
        else
          route_to SanJose                           # all other destinations within US & Canada
      end

      route_to Ribbit, nil, nil     # If GV call failed, try one more time with Ribbit

  else
    rejectCall(603,"Number's too short, check & dial again") if @num.length < 9
    route_to F9default
  end
end

# ------------ O P T I O N A L   C O N F I G U R A T I O N --------------- #

# ********************  i n c o m i n g   C a l l  *************************

def incomingCall
  sys.SetFromHeader(formatNum(@cname || @cid,true), nil, Host)  # Set FromName & FromHost for sys.Dial

  # Forward call to the bindings (ATA / softphone)
  # Change FromURI when forwarding to @local, or else Bria won't find contact in its phonebook!

  callswitch("#{@user}@local[fu=#{@cid}]",45) unless (30..745) === @t.hour*100 + @t.min # reject incoming calls from 0:30a to 7:45a

  @code, @reason = 480, "#{@user} is asleep" unless @code # if nothing else, must be the night hour
  @code = 486 if @trunk =~ /IPCOMM/i ## *** temporary fix for IPCOMMS ***
end

# **************************  t o   E N U M  *******************************

def to_ENUM num
  num.gsub!(/[^0-9*+]/,'') # Delete all fancy chars (only digits, '+' and '*' allowed)

  # Check if the number begins with one of international prefixes:
  #  '+' - international format
  #   00 - European style international prefix (00)
  #  011 - US style international prefix (011)

  num =~ /^(\+|00|011)/ and return $' # if yes, remove prefix and return

  case num                    # Special cases
    when /^[2-9]\d{6}$/       # Local call, 7-digit number
      '1' + Area + num        # prefix it with country and area code
    when /^[01]?([2-9]\d{9})/ # US number with or without "1" country code
      '1' + $1                # add country code and truncate number to 10-digit
    when /^\*/                # Voxalot voicemail, echotest & other special numbers
      num                     # ... as is
    else
      rejectCall(603,"Wrong number: '#{num}', check & dial again")
  end
end

# ****** E N D   O F   C O N F I G U R A T I O N    S E C T I O N ******** #

# **************************  C A L L    S W I T C H  **********************

def callswitch(num,*args)
  @timeout = args[0]
  num.gsub!(/%(..)/) {$1.hex.chr} # Convert %hh into ASCII
  @num = Speeddial[num] || num    # If there is speed dial entry for it...

  if @num =~ /@/              # If we already have URI, just dial and return
    sys.Log("URI dialing: #@num")
    dial(@num,*args)
  else                        # Not URI
    rexp = VSP.tab.keys.sort {|a,b| b.length <=> a.length}.map {|x| Regexp.escape(x)}.join('|')
    if @num =~ /^(#{rexp})/   # If number starts with VSP selection prefix
      @num = $'; @forcedRoute = VSP.tab[$1]
      @noSafeGuards = (@forcedRoute.fmt =~ /Disable\s*Safe\s*Guards/i)
    end

    @num = to_ENUM(@num)      # Convert to ENUM

    rejectCall(503,"Number's empty") if @num.empty?
    sys.Log("Number in ENUM format: #{@num}")
    if @forcedRoute && !@noSafeGuards
      route_to @forcedRoute, "Forced routing!", false # if forced with prefix, skip ENUM, safeguards & VSP selection
    else
      checkNum if defined?(EnableSafeguards) && !@noSafeGuards
      selectVSP               # Pick appropriate provider for the call
    end
  end   # URI
end

# ***************************  R O U T E _ T O  ****************************

def route_to vsp, dest=nil, enum = EnumDB
  enum.to_a.each do |db|   # if enum enabled, look in all enum databases
    if uri = (db.class == Hash)? db[@num] : sys.ENUMLookup("#{@num}.#{db}")
      sys.Log("ENUM entry found: '#{uri}' in #{db.class == Hash ? 'local' : db} database")
      dial(uri); break
    end
  end                   # ENUM not found or failed, call via regular VSP

  return unless vsp     # No VSP - do nothing

  uri = vsp.fmt.gsub(/\s+/,'').gsub(/\$\{EXTEN(:([^:}]+)(:([^}]+))?)?\}/) {@num[$2.to_i,$4? $4.to_i : 100]}
  dest &&= " (#{dest})"; with = vsp.name; with &&= " with #{with}"
  sys.Log("Calling #{formatNum(@num)}#{dest}#{with}")

  if vsp.is_gv?
    vsp.repeat.times do |i|
      @code, @reason = 200, "OK"  # assume OK
      sys.GoogleVoiceCall *vsp.getparams(uri, i + (vsp.rand ? @t.to_i : 0))
      sys.Log("Google Voice Call failed!")
      @code, @reason = 603, 'Service Unavailable'
    end
  else
    vsp.repeat.times do
      dial(uri, @timeout || vsp.tmo || 300) # Dial, global time-out overrides account
    end
  end
end

# *******************************  D I A L  ********************************

def dial *args
  @code, @reason = nil
  sys.Dial *args    # dial URI
  status()          # We shouldn't be here! Get error code...
  sys.Log("Call failed: code #{@code}, #{@reason}")
end

# *****************************  S T A T U S  ******************************

def status
  begin
    @code, @reason = 487, 'Cancelled by Sipsorcery'
    sys.LastDialled.each do |ptr|
      if ptr
        ptr = ptr.TransactionFinalResponse
        @code = ptr.StatusCode; @reason = ptr.ReasonPhrase; break if @code == 200
#       sys.Log("#{ptr.ToString()}")
      end
    end
  rescue
  end
end

# ************************  r e j e c t C a l l  ***************************

def rejectCall code, reason
  @code = code; @reason = reason
  sys.Respond code, reason
end

# ****************************  C H E C K   N U M **************************

def checkNum
  return if @num.match(/^\D/)  # skip if number doesn't begin with a digit

  # Reject calls to not blessed countries and premium numbers
  # (unless VSP was forced using #n dial prefix)

  rejectCall(503,"Calls to code #{formatNum(@num).split(' ')[0]} not allowed") \
    unless @num.match "^(#{Allowed_Country.join('|')})"

  rejectCall(503,"Calls to '#{formatNum($&)}' not allowed") if @num.match \
    '^(' + ExcludedPrefixes.map { |x| "(:?#{x.gsub(/\s*/,'')})" }.join('|') + ')'
end

# **********************  k e y s   t o   E N U M  *************************

def keys_to_ENUM (table)
  Hash[*table.keys.map! {|key| to_ENUM(key.dup)}.zip(table.values).flatten]
end

# **************************  g e t T I M E  *******************************

def getTime
  Time.now + ((Tz+8)*60*60) # Get current time and adjust to local. SS Server is in GMT-8
end

# *******************************  M A I N  ********************************

begin
  sys.Log("** Call from #{req.Header.From} to #{req.URI.User} **")
  sys.ExtendScriptTimeout(15)   # preventing long running dialscript time-out
  @t = getTime()
  sys.Log(@t.strftime('Local time: %c'))
  EnumDB.map! {|x| x.class == Hash ? keys_to_ENUM(x) : x } # rebuild local ENUM table

  if sys.In               # If incoming call...
    @cid = req.Header.from.FromURI.User.to_s    # Get caller ID

    # Prepend 10-digit numbers with "1" (US country code) and remove int'l prefix (if present)

    @cid = ('1' + @cid) if @cid =~ /^[2-9]\d\d[2-9]\d{6}$/
    @cid.sub!(/^(\+|00|011)/,'')   # Remove international prefixes, if any

    prs = req.URI.User.split('.')  # parse User into chunks
    @trunk = prs[-2]               # get trunk name
    @user  = prs[-1]               # called user name

    # Check CNAM first, the Google contacts. If not found and US number, try to lookup caller's name in Whitepages

    if !(@cname = CNAM[@cid]) && (!defined?(GoogleContacts) || !(@cname = sys.GoogleContactLookup(GoogleContacts[:login], GoogleContacts[:pswd], @cid)))
      if !(@cname = keys_to_ENUM(CNAM)[@cid]) && @cid =~ /^1([2-9]\d\d[2-9]\d{6})$/ && defined?(WP_key)
        url = "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20xml%20where%20url%3D'http%3A%2F%2Fapi.whitepages.com%2Freverse_phone%2F1.0%2F%3Fphone%3D#{$1}%3Bapi_key%3D#{WP_key}'%20and%20itemPath%3D'wp.listings.listing'&format=json"
        if js = sys.WebGet(url,4).to_s
          @cname, dname, city, state = %w(businessname displayname city state).map {|x| js =~ /"#{x}":"([^"]+)"/; $1}
          @cname ||= dname; @cname ||= "#{city}, #{state}" if city && state
        end
      end
    end

    sys.Log("Caller's number: '#{@cid}'"); sys.Log("Caller's name:   '#{@cname}'") if @cname
    incomingCall()        # forward incoming call

  else                    # Outbound call ...

    # check if it's URI or phone number.
    # If destination's host is in our domain, it's a phone call

    num = req.URI.User.to_s; reqHost = req.URI.Host.to_s  # Get User and Host
    host = reqHost.downcase.split(':')[0]                 # Convert to lowercase and delete optional ":port"
    num << '@' << reqHost unless Domains.include?(host)   # URI dialing unless host is in our domain list

    callswitch(num)

  end
  sys.Respond(@code,@reason) # Forward error code to ATA
rescue
   # Gives a lot more details at what went wrong (borrowed from Myatus' dialplan)
   sys.Log("** Error: " + $!) unless $!.to_s =~ /Thread was being aborted./
end
*** Update 07/26/2010: Aaron kindly agreed to deploy mikesgem.rb file on the server and I moved my classes and formatNum to that file. Now the configuration section is located in the very top of the dial plan. For those curious, here is a copy of mikesgem.rb.

I also added the Provider() method (see below), you can use it instead of both VSP.new and GV.new.
*** Update 02/08/2011: Fixed a bug (enum parameter of route_to should be nil instead of false, see Aaron's comment on p.6)
*** Update 10/29/2012: Incorporated Google Contacts Lookup for Caller's name
Last edited by MikeTelis on Mon Oct 29, 2012 6:32 am, edited 13 times in total.

MikeTelis
Posts: 1582
Joined: Wed Jul 30, 2008 6:48 am

Re: Flexible table-controlled dialplan, Version 2

Post by MikeTelis » Wed Jul 14, 2010 4:18 am

Introduction

Browsing my phone books, I found that the same phone numbers are stored in various different formats: 370-7070, (408) 370-7070, 1 (408) 370-7070, +14083707070 and so on. The problem is that most of VoIP providers want you to dial a number in international format; and yet some of them expect a certain prefix (like 0, 00 or 011) before the number and/or suffix after. Thus, if I wanted to connect my phone(s) to a VoIP provider and dial numbers directly from my phonebook, I’d have to edit most of the records bringing stored numbers to some common format. Of course, it would take much more time than I was willing to devote to this job.

The idea I had in mind creating this dial plan was to write a code that would let you use your ATA / WiFi phone / VoIP-enabled cellphone in a usual manner; you wouldn't need to change your contacts, phonebooks etc. The code would remove all fancy characters like spaces, dashes and brackets, take care of national / international prefixes, convert local numbers into ENUM format (country code followed by area code and phone number). Then, the dial plan would automatically select the best VoIP provider for your call, optionally add a prefix/suffix and connect your call.

You could also select provider manually by dialing a prefix (such as #5 or **1) before the number.

The code was pre-configured for the U.S. (local 7-digit dialing, long-distance 10- and 11-digit dialing) but it’s easy to adjust it for any country and dial rules.

On incoming calls, the code supports both built-in and White Pages CNAM (Caller’s Name) lookup and will report you the name (or city/state) of your caller, where available.

Features

• DSL/table-controlled, easy to configure
• Speed dial table
• Accepts all number formats (with and without national / international prefix, local numbers, etc
• Supports both regular VoIP providers and Google Voice
• ENUM lookup in worldwide ENUM databases as well as your private database(s)
• Both automatic and manual selection of VoIP providers
• User-controlled failover (switching to alternative provider(s))
• Safeguards (disable calling to certain numbers / number groups, such as premium 1-900, allow calling only to predefined list of countries)
• URI dialing
• CNAM (Caller’s Name) lookup

Overview of dial plan’s structure and methods

The heart of the dial plan is its callswitch method used to relay a call to the destination phone number or URI. It calls user-provided selectVSP method to pick the most appropriate VSP depending on the called number. In turn, selectVSP is using predefined route_to and rejectCall methods for call switching (or rejecting, if the number is incorrect).

The route_to uses either sys.Dial or sys.GoogleVoiceCall system methods (depending on VSP type, regular or Google Voice) to connect the call.

The callswitch is used for both incoming and outgoing calls. Thus, if you need to change the VSP used for termination of local calls, you won’t have to change both inbound and outbound portions of your dial plan; the selectVSP method will be the only one affected.

The callswitch converts dialed number into international (ENUM) format and stores it in the instance variable @num. Conversion occurs at to_ENUM method which can be customized if you need to adopt the dial plan to different country and/or dialing rules.

The incomingCall method is used for incoming call handling. By default, incoming calls are forwarded to your ATA / softphone, but you can change the code and forward incoming calls to your mobile, voicemail, reject calls depending on time of the day etc.

Configuration/customizing overview

All parameters and methods that need configuration/customization are conveniently located within “CONFIGURATION SECTION” of the dial plan. In order to get started, you need to declare (create descriptors) all VoIP providers you’re going to use, both regular and Google Voice accounts. Then you need to edit selectVSP method for optimal routing of your calls. Of course, you need to take care of little things like time zone and area code you live in. Once it’s done, you should be able to make and receive calls.

Next step is filling optional tables: Speeddial, CNAM, MyENUM. If you want to use White Pages for reverse number lookup, you’ll need to get their API key (fill a form, takes a couple of minutes). Probably you need to enable safeguards, they’ll prevent you from inadvertent dialing to some pricey destination, like 1-900-xxx-xxxx.

Finally, you may want to customize routing of incoming calls (forward them to both home and mobile phone on weekends, for example); you’ll need to edit the incomingCall method for that.

You’ll find a detailed description of all these customization procedures later in the document; right now I urge you to take a brief look at the configuration section in order to make yourself accustomed and familiar with the code.

selectVSP, route_to and rejectCall methods

The selectVSP is in charge of routing of your calls. It takes @num as the input and then invokes route_to method to connect the call using one of your VSPs. The best way to explain it is by example. Suppose you have 3 VoIP providers:

UK_Provider
Google_Voice
Other_Provider

UK_Provider offers special rates for calling to UK (country code 44), Google_Voice gives you free calling within the US and Canada (country code 1); you want to use the Other_Provider when calling to all other destinations. Then your selectVSP method could be as follows:

Code: Select all

def selectVSP    # VoIP provider selection
  case @num
    when /^44/                              # United Kingdom
      route_to UK_Provider, "UK"

    when /^1/                               # North America
      route_to Google_Voice, “US / Canada”

    else
      route_to Other_Provider
  end
end
The route_to method takes 3 parameters:

route_to provider, destination, enum

Provider is the only mandatory parameter; it specifies the VSP you want to commit the call with. Destination is basically a comment; it will appear in the Console trace, like this:

Calling +1 (800) 444-4444 (US / Canada) with Google Voice

Enum is a pointer to a list of ENUM databases (if your list contains only one entry, you can specify it literally, e.g. “e164.org” or {‘(408) 555-1212’ => ‘sister@local’}. By default, enum=EnumDB. Route_to will look for called number in given ENUM database(s). If the number appears in an ENUM database, route_to will try to call by SIP URI found in the database and use the provider only if SIP call fails.

If enum is nil, route_to will skip ENUM lookup.

If you want to omit the destination parameter but specify enum, use either empty string or nil as a “placeholder”:

route_to UK_Provider, ‘’, nil
route_to UK_Provider, nil, nil

The other useful method is rejectCall, it takes 2 parameters:

rejectCall code, reason

of which only the first is mandatory. If, for example, you want to reject all calls where destination number’s length is less than 9 digits, insert this line:

rejectCall(603,"Number's too short, check & dial again") if @num.length < 9

just before “route_to Other_Provider”. Keep in mind that @num contains the number in ENUM (full international) format, including country and area code and indeed, it must be 9 digits or more.

The route_to method utilizes sys.Dial or sys.GoogleVoiceCall to initiate the call. If the call is successful (you get connected to the callee), Sipsorcery will stop your dial plan script (all the code below route_to won’t get executed). Should the call attempt fail, your dial plan will regain control and it gives you the opportunity to stay on top of the situation and, for example, try again using a different VSP. Say, if your attempt to connect via UK_Provider failed, you may want to try again with the Other_Provider:

Code: Select all

def selectVSP    # VoIP provider selection
  case @num
    when /^44/                              # United Kingdom
      route_to UK_Provider, "UK"
      route_to Other_Provider, nil, nil

    when /^1/                               # North America
      route_to Google_Voice, “US / Canada”

  else
    route_to Other_Provider
  end
end
Nothing’s too exciting here; you could do the same using failover mechanism (something like sys.Dial(“#@num@UK_Provider|#@num@Other_Provider”). However, this mechanism is only available for sys.Dial (you can’t failover from regular VSP to Google Voice and vice versa). Besides, sometimes you may not want to failover at all. Say, if called number was busy, it wouldn’t be wise to call it again a second later using the other (and probably more expensive VSP). This dial plan provides with a much more flexible options which, btw, can be used on top of the standard sys.Dial failover function.

If route_to failed, you can analyze two variables, @code and @reason to see whether it makes sense trying again using the same VSP, try some other VSP or just give up. @code is an integer; it contains error code returned by the VSP. @reason is a string; it gives a human-readable explanation of the @code. Note that both @code and @reason appear in the Console trace; I suggest that you monitor your dial plan and providers’ behavior for a while to learn what is returned under these or that circumstances (typically results vary, depending on particular VSP) and then make changes to your selectVSP code to adjust it to whatever suits the best for this or that VSP.

The code snippet below will try calling with UK_Provider and failover to Other_Provider unless failure reason contains “busy” (case insensitive):

Code: Select all

      route_to UK_Provider, "UK"
      route_to(Other_Provider) unless @reason =~ /busy/i
VSP descriptors

VSP descriptor keeps various data related to particular VSP; you must have the descriptor for your each and every VSP. You create them by calling either VSP.new or GV.new, depending on the VSP type (regular provider or Google Voice) and save into a variable or constant for future use (as the “provider” parameter of route_to), for example:

Code: Select all

UK_Provider    = VSP.new ‘#1’, ‘${EXTEN}@UKProvider’, ‘UK Provider’

Google_Voice   = GV.new  ‘#2’, nil, ‘Google Voice’, 
                          :usr => ‘myname’,          # Google login name
                          :pwd => ‘password’,        # Google password
                          :cb  => ‘(403) 234-5678’   # Callback number

Other_Provider = VSP.new ‘#3’, ’011 ${EXTEN}@Other’, ‘Other Provider’
Remember that in Ruby, constant names should start with an uppercase letter. Some names are already assigned to my classes and methods and therefore, can’t be used (for example, GV, VSP, Provider).

Both VSP.new and GV.new accept upto 3 ‘fixed’ parameters as well as so-called attributes (parameters in form of :name => value that appear in the end of parameter list). Fixed parameters are:

Prefix, Format, Name

Prefix is an optional dial prefix you can use to bypass automatic provider selection and place the call with this particular provider. For example, if you dial:

#2 44 20 2345-6789

the dial plan will skip selectVSP and route the call to Google Voice passing 442023456789 to it as the number you wish to call.

Format string is used as a template: dial plan will replace ${EXTEN} with the phone number (from @num) before sending it to the provider. You can use this feature to add a prefix and/or suffix to the number. Suppose UK_Provider wants the numbers to be pre-pended with 00, you can do it using ’00 ${EXTEN}@UKProvider’ as the format string. ${EXTEN} has two optional parameters:

${EXTEN:n1:n2}

returns a part of @num starting from offset specified by n1 and of a length specified by n2 (if omitted, to the end of number). For example, if @num is 123456789, then:

$(EXTEN:2) is 3456789
${EXTEN:1:5) is 23456

If offset is negative, it’s counted from the end of the number:

${EXTEN:-7:5} is 34567

Format string is a must parameter for regular VSPs; the dial plan can’t assume it because format string must include the VSP name (as it entered on “SIP providers” page). For Google Voice, format string is optional; if omitted, the dial plan will use ‘+${EXTEN}’ (number prepended with plus sign).

Optional Name parameter is merely a comment; it will appear in the Console trace.

For a Google Voice account, you must specify at least 3 attributes:

Code: Select all

:usr => ‘Google login’,
:pwd => ‘Google password’,
:cb  => ‘Callback number’
One more thing before we proceed to more complicated features of the dial plan: you can use attributes in lieu of fixed parameters, for example:

Code: Select all

UK_Provider    = VSP.new  :pfx  =>‘#1’, 
                          :fmt  => ‘${EXTEN}@UKProvider’, 
                          :name => ‘UK Provider’
or

Code: Select all

UK_Provider    = VSP.new  ‘#1’, ‘${EXTEN}@UKProvider’, :name => ‘UK Provider’
You’ll get the same results regardless of syntax used; it’s just a matter of taste. Note that if you use both fixed parameter and attribute associated with it, fixed parameter has the priority and the attribute will be ignored.

Complete list of attributes is:

Code: Select all

:pfx     => ‘prefix’         # Same as 1st fixed parameter
:fmt     => ‘format’         # Same as 2nd fixed parameter
:name    => ‘name’           # Same as 3rd fixed parameter
:tmo     =>  30              # Time-out
:repeat  =>  1               # Number of retries, default is 1
:rand    =>  true            # Alternate GV accounts at random
:usr     => ‘username’       # Google Voice username
:pwd     => ‘password’       # Google Voice password
:cb      => ‘number’         # Google Voice callback number
:type    =>  Home            # Callback number type: Gizmo, Home, Work, Mobile
:match   => ‘pattern’        # see sys.GoogleVoiceCall match param
:account =>  GVaccounts      # link to GV accounts table
:tmo works differently for regular and GV accounts, it’s connection time-out for regular VSP and callback time-out for GV.

:repeat is the number of connection attempts. Some VSP are experiencing intermittent failures; if this is the case it makes sense to retry 2-3 times.

All other attributes are for Google Voice only.

:type is your callback number’s type, it can be either Home, Work, Mobile or Gizmo. The :type is assumed depending callback number’s area code and usually can be omitted, because only Gizmo makes the difference for Google Voice, the other 3 types work identically. :match is a pattern used for matching callbacks, it’s default value ‘.*’ is good for most users.

To create a descriptor for your typical Google Voice account, you only need to specify :usr, :pwd and :cb. You can do it immediately in GV.new line or create an array with these descriptors and use :account attribute to point to the array:

GVaccount = [
{ :usr => 'login', :pwd => 'pass', :cb => '(206) 242-1234' }
]

Google_Voice = GV.new ‘#2’, nil, ‘Google Voice’, :account => GVaccount

You can break array declaration into 3 lines, if you like this format better:

Code: Select all

GVaccount = [{
  :usr => 'login',         # Your GV login name, with our without @gmail.com
  :pwd => 'pass',          # Your GV password
  :cb  => '(206) 242-1234' # Callback number in 10- or 11-digit notation. 
                           # Add fancy chars to your taste, they are ignored :-)
}]
Personally, I prefer to specify :usr, :pwd and :cb immediately in GV.new line. However, if you used previous version of my dial plan and already have GVaccount array, you’ll save yourself some editing by copy/pasting of GVaccount and pointing to it with :account.

It’s possible to have more than 1 callback number on GV account and use them in sequential or random order (with :repeat and :rand). You can’t specify more than 1 callback number “inline” (in GV.new), you must use GVaccount array instead. Every element of GVaccount array should contain :usr and :pwd and therefore, it's logical to separate common part from variable. So, we declare common part as "Credentials":

Credentials = { :usr => 'login', :pwd => 'pass' } # Your GV login and password

and then use it in each element, adding variable parameters like callback number:

GVaccount = [
Credentials + { :cb => '(206) 242-1234' }, # IPKall callback number
Credentials + { :cb => '(747) 234-5678' }, # Gizmo callback number
]

If you set :rand => true, the starting row in GVaccount array will be selected at random and then the program will try :repeat times. If current row is the last in array, it will proceed to the 1st.

Safeguards

How many times you misdialed a number and ended up calling to some exotic country or a satellite network which had a bad impact on your bill? If none, this topic is probably not for you.

This dial plan is equipped with two safeguards (disabled by default) to prevent you from inadvertent calling to undesired destination.

First is an array of blessed country codes:

Allowed_Country = %w{
1 33 36 39 41 420 44 49 7 86 883 886 90 972
}

If the number you dialed (in ENUM, 'international' format) does not begin with one of these codes, your call will fail with code 503 and "Calls to code ... not allowed" error message. Each element of the array is treated as a regular expression pattern. For example, 37[1-3] will correspond to 371, 372 and 373 country codes.

The second is an array of excluded country / area codes:

Code: Select all

ExcludedPrefixes = [
   ' 1 (900 | 809)',               # USA Premium
   ' 1 \d\d\d 555 1212',           # USA Directory assistance
   '44 (9 | 55 | 70 | 84 | 87)',   # UK Premium
   '49 (1 [^567] | 900)',          # Germany Premium
   '39 (1 | 84 | 89)',             # Italy Premium 
   '420 90',                       # Czech Premium 
]
Each element is a regular expression (or a simple string) defining phone number prefix you'd like to exclude. For example, the first element excludes 1900 and 1809 numbers. Spaces are ignored, so feel free to use them to make the patterns more readable.

If dialed number matches one of the ExcludedPrefixes patterns, the call will fail with 503 code and 'Calls to ...* not allowed' message.

What if you need to call one of prohibited numbers and you don't have computer handy to edit your dial plan? Well, you can use a prefix to force VoIP provider. In this case, the dial plan will bypass safeguard checks and your call will be routed to provider specified by the prefix. It’s also possible to create a dedicated prefix which would disable safeguards for the number:

Code: Select all

            VSP.new '**', 'DisableSafeGuards'     # Disable safeguards prefix is **
It’s a VSP descriptor containing “disable safe guards” (case not significant, spaces ignored). The only field that matters (except format) is the prefix, star-star in our case. If you prepend the number with ** it will bypass the Safeguards and (unlike forceful VSP selection) use regular (selectVSP) routing.

Finally, don't forget to uncomment EnableSafeguards:

# Uncomment line below to enable misdialing safeguards

EnableSafeguards = 1

Caller’s Name lookup

The dial plan is equipped with CNAM feature, it will look up for caller’s name in built-in table and (if not found) in the White Pages. If the number is unlisted, it will report caller’s city and state instead of the name. White Pages search works only if the caller is in the U.S.

To enable the feature, you need to populate CNAM hash table with phone numbers and names of your callers. The numbers must be in the same format as you dial them; the dial plan uses to_ENUM method to convert the numbers. Thus, if you’re in the area code 408, the number +1 (408) 370-7070 can be encoded in any of these formats:

‘370-7070’
‘(408) 370-7070’
‘1 (408) 370-7070’
‘+1 (408) 370-7070’

If you wish to use White Pages, you need to obtain White Pages API key at:

http://developer.whitepages.com/

and insert it where indicated (right below CNAM hash table). Don't forget to remove the pound sign in the beginning of WP_key line to uncomment it!

ENUM lookup

The dial plan is searching ENUM databases in order as they appear in the EnumDB array. The elements of this array are either strings containing ENUM server name or hash table containing your private ENUM entries. Thus, you can use your own ENUM table to convert phone numbers into SIP URI. For example, if you know that your brother is on sipsorcery.com and his SIP account name is “brother”, you can always call him free of charge (SIP-to-SIP call), the dial plan will automatically substitute his phone number with ‘brother@local’.

Sometimes you may want to disable ENUM function. For example, all U.S. toll-free numbers are listed in ENUM database and therefore, if you dial 1-800-xxx-xxxx the call will be routed via one of toll-free VoIP gateways rather than your regular provider such as Google Voice. The problem is that in this case your callee won’t be able to identify you by caller ID. It may be important if you’re calling your bank and they have your Google Voice number on file.

You can disable ENUM for certain numbers (or completely) by using nil as the 3rd parameter of route_to, as described before.

incomingCall method

The dial plan invokes incomingCall() when it needs to forward inbound call to your desired destination. At entry, the following variables are defined:

Code: Select all

@cid   – Caller ID; typically contains caller’s number converted into ENUM format but may 
         also contain “Unknown”, “Private”, “Anonymous” and alike if caller ID’s restricted
@cname – Caller’s Name, where available
@user  – Called SIP account name
@trunk – arbitrary prefix used to identify DID
@t     - local time the call was received at
If call forward failed, incomingCall must set @code and @reason and return; the dial plan will reject incoming call with given @code and @reason. Note that if you use callswitch to forward the call, @code and @reason will be set automatically.

The “default” incomingCall method forwards all incoming calls to the bindings (softphone/ATA registered to Sipsorcery SIP account) during regular hours (between 7:45 and 00:30); during night hours, all calls are rejected with 480 error code.

A couple of words re: @user and @trunk. Suppose your SIP account’s name is ‘myname’ and your DID is forwarding incoming calls to ‘myname@sipsorcery.com’. If you have several DIDs, all sending calls to the same SIP account, you may need to distinguish between them. Sipsorcery provides with suffix-matching mechanism; you can use arbitrary prefix before ‘myname’, for example: IPCOMMS.myname@sipsorcery.com. When you get the call from this DID, @trunk will be set to ‘IPCOMMS’ and @user will receive ‘myname’.

to_ENUM method

The to_ENUM method is called to convert dialed number into international (ENUM) format:

@num = to_ENUM(number)

The “default” code takes care of international prefixes (+, 00, 011), 7-digit and 10-digit numbers.

Configuration example

The dial plan comes in the following (rather elaborated) configuration:

• Area code 408
• Time zone GMT-8 (PST)
• Four regular VoIP providers (Future-Nine, Voxalot, Gizmo5 and Ribbit)
• Three Google Voice accounts (San Jose, San Francisco and Chicago)

Future-Nine is presented in 4 variations: default, grey, white and premium route. Prefixes are #0, #2, #3 and #4, respectively.

Google Voice accounts are automatically selected, depending on the area code you dialed. For example, if you dial to San Francisco, the dial plan will select GV account with phone number in (415) area code. Should the called want to call you back, it will be a local call for him or her.

If Google Voice call failed, the dial plan will try to call with Ribbit.

Please note that GV accounts can be selected by prefixes, too (*#1, *#2, *#3) and I use this in Speeddial table. There are 3 entries for calling GV Voice Mail. As you probably know, it’s impossible to call your own Google Voice number using GV account to which it belongs. That’s why I use GV account with San Francisco number to call GV number in San Jose, and I select this account using *#2 prefix.


Last minute changes

1. Added a wrapper method for VSP.new and GV.new. Now you can use Provider instead and it will invoke either VSP.new or GV.new, depending on parameters. Examples:

Code: Select all

ActionVoip = Provider ‘#1’, ‘ ${EXTEN}@Actionvoip’, ‘ActionVoip’
will be translated into:

Code: Select all

ActionVoip = VSP.new ‘#1’, ‘ ${EXTEN}@Actionvoip’, ‘ActionVoip’

Code: Select all

Google = Provider ‘#2’, nil, ‘My Google Voice’, :usr => ‘login’, :pwd => ‘password’,
                                                :cb  => ‘(253) 444-7890’
will be translated into:

Code: Select all

Google = GV.new ‘#2’, nil, ‘My Google Voice’, :usr => ‘login’, :pwd => ‘password’,
                                              :cb  => ‘(253) 444-7890’
2. If “provider” (1st parameter) of route_to is nil, it will only search ENUM database and route the call to ENUM entry (if found). If the number is not found in ENUM database or database search is prohibited, route_to will do nothing.
Last edited by MikeTelis on Tue Feb 08, 2011 1:08 pm, edited 10 times in total.

MikeTelis
Posts: 1582
Joined: Wed Jul 30, 2008 6:48 am

Re: Flexible table-controlled dialplan, Version 2

Post by MikeTelis » Wed Jul 14, 2010 4:50 am

For those who find GV.new and VSP.new inconvenient, add the following 3 lines:

Code: Select all

def Provider(*args)
  a = args.last; a.class == Hash && (a[:usr] || a[:account]) ? GV.new(*args) : VSP.new(*args)
end
right after GV class definition (before # **** .......................... to here ******************************** # comment) and you'll be able to use Provider instead of both GV.new and VSP.new:

Code: Select all

UK_Provider    = Provider ‘#1’, ‘${EXTEN}@UKProvider’, ‘UK Provider’

Google_Voice   = Provider ‘#2’, nil, ‘Google Voice’, 
                          :usr => ‘myname’,          # Google login name
                          :pwd => ‘password’,        # Google password
                          :cb  => ‘(403) 234-5678’   # Callback number

Other_Provider = Provider ‘#3’, ’011 ${EXTEN}@Other’, ‘Other Provider’

beaver
Posts: 241
Joined: Tue Feb 09, 2010 5:32 am
Location: Beaverton USA (PST, GMT - 8)

Re: Flexible table-controlled dialplan, Version 2

Post by beaver » Wed Jul 14, 2010 9:26 pm

Mike, can you also add date stamp at the first lime when you update the test plan so that we could know the date it's updated. 2010 is pretty wide time range.

MikeTelis
Posts: 1582
Joined: Wed Jul 30, 2008 6:48 am

Re: Flexible table-controlled dialplan, Version 2

Post by MikeTelis » Wed Jul 14, 2010 9:28 pm

Sure, no problem. Probably I'll move the code to Google Code or some other SVN server so it will be easy to track the changes.

Update 7/20/2010: Almost cosmetic change, sys.Log message when ENUM record is found in local hash table didn't look good.

MikeTelis
Posts: 1582
Joined: Wed Jul 30, 2008 6:48 am

Re: Flexible table-controlled dialplan, Version 2

Post by MikeTelis » Thu Jul 22, 2010 4:13 am

Updated both the code and the manual. Changes:

1. Introduced incomingCall method containing the code you can customize to change routing of your incoming calls. For example, you can send the call to ATA and start calling your cellphone 10 sec later, if there's no answer on ATA; you can have different incoming call schemes for day / night / weekends etc.

2. Introduced to_ENUM method converting dialed number into international (ENUM) format. Typically you need to change this porting the dialplan for some other (than the U.S.) country.

Both methods are conveniently located within configuration section of the dialplan.

3. CNAM and local ENUM tables may contain "formatted" numbers in the same format as you dial them (for example, 7-digit for local numbers). The dialplan is using to_ENUM to convert them into ENUM format.

4. The 3rd parameter of route_to (enum) has become more flexible, you can specify which ENUM database to use (read the manual and check the code).

nozyczek
Posts: 8
Joined: Sun Jul 25, 2010 1:26 pm

Re: Flexible table-controlled dialplan, Version 2

Post by nozyczek » Sun Jul 25, 2010 1:47 pm

MikeTelis,
I would like to thank you for sharing this great dial plan. I have only one GV number and one Sipgate number but with your instructions and comments had no problems to make necessary changes to make it work.
I'm completely new to all of this and would like to ask you two question. Both questions are related to rejecting incoming calls between certain hours.

1. Right now when "i'm asleep" my calls are transfered to sipgate voicemail. Is there any chance to transfer them to google voicemail instead? It is not a big of a deal but I would like to keep everything in my google voice if possible.
2. Is it possible to incorporate whitelist numbers ... in other words, even if "i'm asleep" selected calles would go through?

Thanks again for this great dial plan.
nozyczek

MikeTelis
Posts: 1582
Joined: Wed Jul 30, 2008 6:48 am

Re: Flexible table-controlled dialplan, Version 2

Post by MikeTelis » Sun Jul 25, 2010 3:06 pm

nozyczek wrote:1. Right now when "i'm asleep" my calls are transfered to sipgate voicemail. Is there any chance to transfer them to google voicemail instead?
I don't have Sipgate account and can't give you detailed instructions. Generally, you need to disable Sipgate voicemail at all, hope you'll find this option in your Sipgate account's settings.
2. Is it possible to incorporate whitelist numbers ... in other words, even if "i'm asleep" selected calles would go through?
I deliberately pulled up related code into incomingCall() method. If someone needs to change handling of incoming calls, he or she should know where it is :-)
First, you need to define your WhiteList:

Code: Select all

WhiteList = [
   '(408) 454-4455',
   '(212) 555-1212',
]
Then you modify a line in incomingCall from:

Code: Select all

  callswitch("#{@user}@local[fu=#{@cid}]",45) unless (30..745) === @t.hour*100 + @t.min # reject incoming calls from 0:30a to 7:45a
to

Code: Select all

  if !((30..745) === @t.hour*100 + @t.min) or WhiteList.map {|n| to_ENUM(n)}.include?(@cid)
     callswitch("#{@user}@local[fu=#{@cid}]",45)
  end

nozyczek
Posts: 8
Joined: Sun Jul 25, 2010 1:26 pm

Re: Flexible table-controlled dialplan, Version 2

Post by nozyczek » Sun Jul 25, 2010 4:00 pm

Thanks for your super fast response. I will follow your suggestions and will report back

nozyczek
Posts: 8
Joined: Sun Jul 25, 2010 1:26 pm

Re: Flexible table-controlled dialplan, Version 2

Post by nozyczek » Mon Jul 26, 2010 3:18 pm

MikeTelis,
Looks like everything will work as you said :) I still need to do more testing but everything looks good so far.
I do have another question. I was wondering if there is any way to include a group of numbers ... something like this:

Code: Select all

WhiteList = [
'(403) 111-1111', # my cell
'(403) 111-2222', # my work
'(403) 222-*',       # my group of numbers
]
I tried a few things including "*" and "????" but keep getting the same "Wrong number" error

Code: Select all

DialPlan 14:43:36:620 sip1: UAS call failed with a response status of 603 and Wrong number: '403222', check & dial again.
Would you have suggestion how to achieve this?
Thanks
nozyczek

Post Reply