This is a Google Voice outbound dial plan created to provide a few useful features and a simplicity in configuration and usage. The main features are a completely phone-controlled speed dial function, forwarding phone selector, forwarding phone weighted randomizer, and pseudo 2-line phone. Other features included are a fallback phone, phone number formatter, and non-free call blocker. All the configuration settings are at the top of the dial plan.
Speed Dial
Speed dial numbers are stored and deleted, as well as dialed, using the phone. Upon each store/delete an updated speed dial list is emailed to the user. The speed dial list can also be emailed at any time by request of the user. Note: to use the email part of the speed dial feature, authorization must first be acquired from the SIPSorcery admin to use the SIPSorcery Email application. Information about the Email application can be found on this help page.
Speed dial numbers can have a maximum of 3 digits, providing an available number range of 0 to 999. There are approximately sixty speed dial slots available. The amount is dictated by the maximum size of a SIPSorcery database value, which is currently 1k.
Forwarding Phone Selector
Selection of the forwarding phone is done by phone. The user can set/change the default forwarding phone and, also, specify a particular forwarding phone to use on a per-call basis.
Forwarding phone keys are 1 digit in the range of 1 to 9, providing the availability of up to nine forwarding phones for Google Voice callback.
Forwarding Phone Weighted Randomizer
The weighted randomizer provides an alternative way to juggle forwarding phones rather than explicitly selecting them using the forwarding phone selector. Usage is very similar to the forwarding phone selector in that it, also, is set using the phone, it shares the same modifier key for phone entry, and the range of allowable numbers is 1 to 9. Where they differ is in the number of digits entered.
The weighted randomizer requires a minimum of two digits entered, anything less and that would be a selection. While the minimum is two digits, there is no defined maximum, and number repetition is not only allowed, it is an integral part of the feature. Number repetition is how "weight" is applied to the randomizer. The more times a specific number is entered into the randomizer, the greater the chance that the forwarding phone represented by that number will be used, and vice versa.
Pseudo 2-Line Phone
Calls can be dialed via either of two different Google Voice accounts. Line 1 is the default line and requires no special action to place calls on it. Line 2 calls are placed by appending an asterisk at the end of the number/string to be dialed.
Fallback Phone
When the fallback phone feature is enabled, a second callback attempt will be made in the event that the first does not connect. Fallback phone will always use a different forwarding phone, if available, than the one used for the initial callback attempt. If forwarding phones are being randomized with the weighted randomizer, another random phone will be used, else fallback phone uses the foremost unused forwarding phone.
Phone Number Formatter
11 digit numbers are left unchanged: ..................................................... 1(222)333-4444 --> 1(222)333-4444
10 digit numbers will be prefixed with a 1: ............................................... (222)333-4444 --> 1(222)333-4444
7 digit numbers will be prefixed with a 1 and the local area code: .................... 333-4444 --> 1(222)333-4444
Vanity numbers will have extraneous digits removed: .......................... 1(800)PIZZANOW --> 1(800)749-9266
International numbers will have the 011 replaced with a plus sign: ... 011 44 111222333 --> +44 111222333
Non-Free Call Blocker
All non-free numbers can be blocked, limiting allowable calls to Google Voice free calling destinations (currently USA and Canada) and disallowing calls to 900/976 numbers. The call blocker also includes a feature to pop holes in it, allowing non-free calls to only countries and territories explicitly specified by the user.
Dial Plan Usage
[Key: xxxxxxx = phone number | xxx = speed dial number | x = forwarding phone key(s)]
Dial a number, using the default (or randomized) forwarding phone¹:
xxxxxxx
Dial a number, using a specified forwarding phone¹:
xxxxxxx*x
Dial a speed dial number, using the default (or randomized) forwarding phone¹:
xxx
Dial a speed dial number, using a specified forwarding phone¹:
xxx*x
Set default forwarding phone or set weighted randomizer²:
*x
Store a speed dial number:
xxx#xxxxxxx
Delete a speed dial number:
xxx#000
Delete multiple speed dial numbers:
xxx*xxx*xxx#000
Delete a range of speed dial numbers:
xxx**xxx#000
Request speed dial list:
000
Notes:
¹ To call out on line 2 append an asterisk(*) at the end of the dialed number/string.
² An alternative entry method for the forwarding phone selector/randomizer is **x. This alternative is available for users who have a vertical service code conflict when entering a single asterisk followed by two digits.
Code: Select all
#####################################################################
################# GOOGLE VOICE OUTBOUND DIAL PLAN #################
#####################################################################
## SIPSorcery Ruby dial plan for routing outbound calls via ##
## Google Voice featuring a completely phone-controlled ##
## speed dial function, forwarding phone selector, forwarding ##
## phone weighted randomizer, and 2-line Google Voice account ##
## selector. Other features are fallback phone, phone number ##
## formatter and call blocker. ##
#####################################################################
# SIP tracing [true|false]
sys.Trace = false
# Google Voice account credentials
# GV syntax: GV[1..2] = ["username", "password", "number"]
GV = Hash.new # Create object for accounts
GV[1] = ["username1", "password1", "2223334444"]
GV[2] = ["username2", "password2", "3334445555"]
# Google Voice forwarding phones
# PHONES syntax: PHONES[1..9] = ["phone", phoneType]
# phoneType key: [ 1:Home | 2:Mobile | 3:Work | 7:Gizmo ]
PHONES = Hash.new # Create object for phones
PHONES[1] = ["17473334444", 7] # Gizmo5
PHONES[2] = ["3603334444", 1] # IPKall
PHONES[3] = ["4153334444", 1] # Sipgate
# Time zone settings
# TIME_ZONE: standard time UTC offset
# DST_OBSERVED: locale observes daylight saving time [true|false]
TIME_ZONE = -5 # EST (UTC-5)
DST_OBSERVED = true # DST is observed in New York
# Speed dial list email address
# Email sent upon speed dial number store, delete and by request.
# NOTE: this feature requires authorization from the SIPSorcery admin
# to use the SIPSorcery Email application.
EMAIL_ADDRESS = ""
# Allow calls only to free calling destinations [true|false]
# Free calling destinations are USA and Canada.
# Blocks calls to premium-rate 900/976 numbers.
FREE_CALLS_ONLY = true
# Enable fallback phone [true|false]
# Enable another callback attempt in the event that the first does not
# connect. Fallback phone will always use a different forwarding phone
# than used for the initial callback attempt, if available.
# If randomizing forwarding phones, another random phone will be used,
# else uses the foremost unused forwarding phone.
ENABLE_FALLBACK_PHONE = false
#####################################################################
### OPTIONAL SETTINGS
# Adjust the default callback timeout value that is in effect when
# fallback phone is enabled [default: 10]
# Timeout values can also be set per phone as a third parameter on
# each forwarding phone contained in the PHONES hash.
# All timeout adjustments apply only when fallback phone is enabled.
#CALLBACK_TIMEOUT = 10
# Manually set local area code and bypass the dial plan logic that
# determines local area code from the Google Voice number.
#AREA_CODE = ""
# Override FREE_CALLS_ONLY to allow international calls to countries
# specified by country code.
# Ex: INTL_COUNTRY_CODE_OVERRIDE = "44, 91" #allow calls to UK and India
#INTL_COUNTRY_CODE_OVERRIDE = ""
# Override FREE_CALLS_ONLY to allow non-free North American Numbering
# Plan calls to countries and territories specified by area code.
# Ex: NANP_AREA_CODE_OVERRIDE = "787, 939" #allow calls to Puerto Rico
#NANP_AREA_CODE_OVERRIDE = ""
#####################################################################
### DIAL PLAN LOGIC #################################################
#####################################################################
#####################################################################
### Regex patterns and data
PHONE_NUMBER_PATTERN = '(011\d+|(?:1?[2-9]\d{2})?[2-9]\d{6})\d*'
SPEED_DIAL_PATTERN = '\d|[1-9]\d{1,2}'
FWD_PHONE_PATTERN = '\*\*?([1-9]+)'
PREMIUM_RATE_PATTERNS = [
'1?900', # 900 numbers
'(?:1?[2-9]\d{2})?976\d{4}', # 976 numbers
]
NANP_NON_FREE_AREA_CODES = [
'684', # American Samoa
'264', # Anguilla
'268', # Antigua & Barbuda
'242', # Bahamas
'246', # Barbados
'441', # Bermuda
'345', # Cayman Islands
'767', # Dominica
['809','829','849'], # Dominican Republic
'473', # Grenada
'671', # Guam
'876', # Jamaica
'664', # Montserrat
'670', # Northern Mariana Islands
['787','939'], # Puerto Rico
'869', # Saint Kitts and Nevis
'758', # Saint Lucia
'784', # Saint Vincent and the Grenadines
'721', # Sint Maarten
'868', # Trinidad and Tobago
'649', # Turks & Caicos
'284', # Virgin Islands (British)
'340', # Virgin Islands (US)
]
#####################################################################
### Define methods
# userTime(time_zone_int=0, dst_observed_bool=false) -> time
def userTime(tz=0, dst=false)
@time = Time.new
@offset = (dst && @time.dst? ? tz.to_i + 1 : tz.to_i) * 3600
@time = @time.utc + @offset
end
# getHash(db_key_str) -> hash
# Read string from database, convert string into hash
def getHash(k)
@h = sys.DBRead(k).to_s
Hash[*@h.split(',')]
end
# putHash(db_key_str, hash, option_str=nil) -> nil
# Convert hash into string, write string to database
# Option: [ sort : sort hash by key ]
def putHash(k, h, option=nil)
case option
when 'sort'
@v = h.sort {|a,b| a[0].to_i <=> b[0].to_i }.join(',')
else
@v = h.to_a.join(',')
end
sys.DBWrite(k,@v)
end
# filterPhoneNumber(phone_number_str) -> phone number string or abort call
def filterPhoneNumber(phone_number)
if FREE_CALLS_ONLY
# Apply international call overrides, if any
if defined?(INTL_COUNTRY_CODE_OVERRIDE) && !INTL_COUNTRY_CODE_OVERRIDE.empty?
@intl_a = "(?!#{INTL_COUNTRY_CODE_OVERRIDE.strip.gsub(/\D+/, '|')})"
else
@intl_a = ''
end
# Apply non-free NANP call overrides, if any
if defined?(NANP_AREA_CODE_OVERRIDE) && !NANP_AREA_CODE_OVERRIDE.empty?
@nanp_a = NANP_AREA_CODE_OVERRIDE.strip.gsub(/\D+/, '|')
@nanp_na = NANP_NON_FREE_AREA_CODES.flatten.delete_if {|x| x =~ /#{@nanp_a}/ }.join('|')
else
@nanp_na = NANP_NON_FREE_AREA_CODES.flatten.join('|')
end
# Filter phone number through non-free call blocker
case phone_number
when /^(?:011#{@intl_a}|1?(?:#{@nanp_na})\d{7})/
sys.Log("Toll calls to #{phone_number} are not allowed.\t")
sys.Respond(403, "Call Not Allowed")
when /^(?:#{PREMIUM_RATE_PATTERNS.join('|')})/
sys.Log("Premium-rate calls to #{phone_number} are not allowed.\t")
sys.Respond(403, "Call Not Allowed")
end
end
# Format phone number
if phone_number =~ /^011/
# Apply international number formatting
return phone_number.sub(/^011/, '+')
else
# Determine area code
if defined?(AREA_CODE) && !AREA_CODE.empty?
@area_code = AREA_CODE
else
@area_code = GV[GV_KEY][2][/^1?(\d{3})/, 1]
end
# Apply NANP number formatting, as needed
return phone_number.rjust(11, "1#{@area_code}")
end
end
# callGoogleVoice(phone_number_str, fwd_phone_array, timeout_int=30) -> nil
def callGoogleVoice(phone_number, phone, timeout=30)
if ENABLE_FALLBACK_PHONE
timeout = phone[2] || ( defined?(CALLBACK_TIMEOUT) ? CALLBACK_TIMEOUT : 10 )
end
sys.Log('-' * 50 + "\t")
sys.Log(" Dialing via Google Voice: #{phone_number}\t")
sys.Log(" Google Voice account: [#{GV_KEY}] #{GV[GV_KEY][2]}\t")
sys.Log(" Forwarding phone: [#{PHONES.index(phone)}] #{phone[0]}\t")
sys.Log('-' * 50 + "\t")
sys.GoogleVoiceCall(GV[GV_KEY][0], GV[GV_KEY][1], phone[0], phone_number, GV[GV_KEY][2], phone[1], timeout)
end
# emailSpeedDial() -> nil
def emailSpeedDial
unless defined?(EMAIL_ADDRESS) && !EMAIL_ADDRESS.empty? then return end
@speed_dial = getHash('speed_dial')
@title = "SIPSorcery Speed Dial List for #{sys.Username}"
@time = userTime(TIME_ZONE, DST_OBSERVED).strftime("%b %d, %Y - %I:%M:%S %p")
# Format speed dial list
@speed_dial_list = Array.new
@speed_dial.each_pair {|k,v|
k = "[#{k}]".rjust(5, ' ')
v = v.sub(/^1([2-9]\d{2})([2-9]\d{2})(\d{4})$/, '1(\1) \2-\3')
@speed_dial_list.push("#{k} #{v}")
}
# Format email body
@body = String.new
@body << "\n" * 2
@body << @title.center(72)
@body << "\n" * 2
@body << @time.center(72)
@body << "\n" * 4
@list_length = @speed_dial_list.length
@entry_lengths = @speed_dial_list.map {|v| v.length }
@entry_lengths_max = @entry_lengths.max.to_i
case @entry_lengths_max > 28 ? 1 : @list_length
when 0
@body << 'The speed dial list is empty.'.center(72)
when 1..15
@margin = [[
(( 72 - ( @entry_lengths.inject(0.0) {|sum,v| sum + v } / @list_length )) / 2 ).floor,
72 - @entry_lengths_max,
].min, 8].max
until @speed_dial_list.empty?
@body << ' ' * @margin
@body << @speed_dial_list.shift.slice(0, 68 - @margin)
@body << "\n"
end
when 16..1.0/0
until @speed_dial_list.empty?
@body << ' ' * 8
@body << @speed_dial_list.shift.ljust(32, ' ')
@body << @speed_dial_list.slice!(@list_length / 2).to_s
@body << "\n"
end
end
@body << "\n" * 5
sys.Email(EMAIL_ADDRESS, "#{@title} (#{@time})", @body)
end
#####################################################################
### Prepare dialed number
dialed_number = req.URI.User.to_s
# Convert HEX(%hh) into ASCII
if dialed_number =~ /%/
sys.Log("Dialed number converted: from: #{dialed_number}\t")
dialed_number.gsub!(/%(..)/) {$1.hex.chr}
sys.Log("Dialed number converted: to: #{dialed_number}\t")
end
# Select Google Voice account
GV_KEY = dialed_number.slice!(/\*$/) ? 2 : 1
#####################################################################
### Process dialed number
case dialed_number
when /^(?:#{PHONE_NUMBER_PATTERN}|(#{SPEED_DIAL_PATTERN}))(?:#{FWD_PHONE_PATTERN})?$/
# Dial phone number or speed dial number
# Option: specify forwarding phone for this call
if $2
speed_dial = getHash('speed_dial')
if phone_number = speed_dial[$2]
sys.Log("Calling speed dial number: [#{$2}] #{phone_number}\t")
else
sys.Log("Speed dial number not found for key: [#{$2}]\t")
sys.Respond(404, "Speed Dial Number Not Found")
end
else
phone_number = filterPhoneNumber($1)
end
phones_key = ( $3 || sys.DBRead('fwd_phone') ).to_i
unless phone = PHONES[phones_key]
if phones_key.to_s.length > 1
randomizer = phones_key.to_s.split(//).map {|s| s.to_i }
randomizer.delete_if {|i| !PHONES.key? i }
if randomizer.empty?
sys.Log("Forwarding phone randomizer contains no valid keys: [#{phones_key}]\t")
sys.Respond(400, "Invalid Forwarding Phone Randomizer Keys")
else
phones_key = randomizer.slice(rand(randomizer.length))
phone = PHONES[phones_key]
sys.Log("Forwarding phone randomized among the following keys: [#{randomizer}]\t")
end
elsif phones_key.zero?
phone = PHONES.values.first
sys.Log("Default forwarding phone not set, using first phone found.\t")
else
sys.Log("Forwarding phone not found for key: [#{phones_key}]\t")
sys.Respond(404, "Forwarding Phone Not Found")
end
end
callGoogleVoice(phone_number, phone)
if ENABLE_FALLBACK_PHONE
sys.Log("Call did not connect, trying fallback phone.\t")
if randomizer && !randomizer.delete_if {|i| phones_key == i }.empty?
phone = PHONES[randomizer.slice(rand(randomizer.length))]
sys.Log("Fallback phone randomized among the following keys: [#{randomizer}]\t")
else
PHONES.length > 1 && PHONES.delete(phones_key)
phone = PHONES.values.first
end
callGoogleVoice(phone_number, phone)
end
when /^(#{SPEED_DIAL_PATTERN})##{PHONE_NUMBER_PATTERN}$/
# Store speed dial number
phone_number = filterPhoneNumber($2)
speed_dial = getHash('speed_dial')
speed_dial.store($1, phone_number)
putHash('speed_dial', speed_dial, 'sort')
sys.Log("Speed dial number stored: [#{$1}] #{phone_number}\t")
emailSpeedDial
sys.Respond(487, "Speed Dial Number Stored")
when /^((?:#{SPEED_DIAL_PATTERN})(?:(?:\*(?:#{SPEED_DIAL_PATTERN}))*|\*\*(?:#{SPEED_DIAL_PATTERN})))#000$/
# Delete speed dial number(s)
speed_dial = getHash('speed_dial')
target = $1
if target =~ /\*\*/
range = true
target = target.split('**').map {|s| s.to_i }
if target[0] < target[1]
speed_dial.delete_if {|k,v| (target[0]..target[1]).include? k.to_i }
else
sys.Log("Speed dial deletion invalid key range: [#{target.join('] to [')}]\t")
sys.Respond(400, "Invalid Speed Dial Key Range")
end
else
range = false
target = target.split('*').uniq.map {|s| s.to_i }.sort
speed_dial.delete_if {|k,v| target.include? k.to_i }
end
putHash('speed_dial', speed_dial)
sys.Log("Speed dial number(s) deleted for key(s): [#{range ? target.join('] to [') : target.join('] [')}]\t")
emailSpeedDial
sys.Respond(487, "Speed Dial Number(s) Deleted")
when /^#{FWD_PHONE_PATTERN}$/
# Set default or randomize forwarding phone
if $1.length > 1
# Set forwarding phone randomizer
randomizer = $1
orphan_keys = randomizer.split(//).uniq.select {|s| PHONES[s.to_i].nil? }
if !orphan_keys.empty?
sys.Log("Some randomizer keys have no matching forwarding phone: [#{orphan_keys.join('] [')}]\t")
sys.Log("Keys will be stored as entered, randomizer may not perform as expected.\t")
end
sys.DBWrite('fwd_phone', randomizer)
sys.Log("Forwarding phone randomizer set: [#{randomizer}]\t")
sys.Respond(487, "Forwarding Phone Randomizer Set")
else
# Set forwarding phone
phones_key = $1.to_i
if PHONES[phones_key]
sys.DBWrite('fwd_phone', phones_key.to_s)
sys.Log("Forwarding phone set: [#{phones_key}] #{PHONES[phones_key][0]}\t")
sys.Respond(487, "Forwarding Phone Set")
else
sys.Log("Forwarding phone not found for key: [#{phones_key}]\t")
sys.Respond(404, "Forwarding Phone Not Found")
end
end
when /^000$/
# Email speed dial list
emailSpeedDial
sys.Respond(487, "Speed Dial List Request Processed")
end
#####################################################################
----------------------------------------------------------------------------------------------------
Update [2009_12_02]
Fix: the daylight saving time adjustment in the userTime method was corrected to jump ahead one hour, rather than fall back one hour.
----------------------------------------------------------------------------------------------------
Update [2009_12_07]
New: added Forwarding Phone Weighted Randomizer and Fallback Phone. These new features were inspired by Mike Telis' Alternating Google Voice Accounts dial plan.
----------------------------------------------------------------------------------------------------
Update [2009_12_12]
Fix: fallback phone was functioning only when using the forwarding phone weighted randomizer. It has been corrected to also function with the forwarding phone selector.
----------------------------------------------------------------------------------------------------
Update [2010_02_14]
New: added Pseudo 2-Line Phone.
----------------------------------------------------------------------------------------------------