jicksta-adhearsion 0.7.999
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +6 -0
- data/EVENTS +11 -0
- data/LICENSE +456 -0
- data/README.txt +5 -0
- data/Rakefile +120 -0
- data/adhearsion.gemspec +146 -0
- data/app_generators/ahn/USAGE +5 -0
- data/app_generators/ahn/ahn_generator.rb +87 -0
- data/app_generators/ahn/templates/.ahnrc +34 -0
- data/app_generators/ahn/templates/README +8 -0
- data/app_generators/ahn/templates/Rakefile +23 -0
- data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
- data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/config.yml +12 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
- data/app_generators/ahn/templates/components/restful_rpc/README.markdown +11 -0
- data/app_generators/ahn/templates/components/restful_rpc/config.yml +34 -0
- data/app_generators/ahn/templates/components/restful_rpc/example-client.rb +48 -0
- data/app_generators/ahn/templates/components/restful_rpc/restful_rpc.rb +87 -0
- data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
- data/app_generators/ahn/templates/config/startup.rb +53 -0
- data/app_generators/ahn/templates/dialplan.rb +3 -0
- data/app_generators/ahn/templates/events.rb +32 -0
- data/bin/ahn +28 -0
- data/bin/ahnctl +68 -0
- data/bin/jahn +42 -0
- data/examples/asterisk_manager_interface/standalone.rb +51 -0
- data/lib/adhearsion/cli.rb +223 -0
- data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
- data/lib/adhearsion/component_manager.rb +208 -0
- data/lib/adhearsion/events_support.rb +84 -0
- data/lib/adhearsion/foundation/all.rb +9 -0
- data/lib/adhearsion/foundation/blank_slate.rb +5 -0
- data/lib/adhearsion/foundation/custom_daemonizer.rb +45 -0
- data/lib/adhearsion/foundation/event_socket.rb +203 -0
- data/lib/adhearsion/foundation/future_resource.rb +36 -0
- data/lib/adhearsion/foundation/global.rb +1 -0
- data/lib/adhearsion/foundation/metaprogramming.rb +17 -0
- data/lib/adhearsion/foundation/numeric.rb +13 -0
- data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
- data/lib/adhearsion/foundation/relationship_properties.rb +42 -0
- data/lib/adhearsion/foundation/string.rb +26 -0
- data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
- data/lib/adhearsion/foundation/thread_safety.rb +7 -0
- data/lib/adhearsion/host_definitions.rb +67 -0
- data/lib/adhearsion/initializer/asterisk.rb +81 -0
- data/lib/adhearsion/initializer/configuration.rb +254 -0
- data/lib/adhearsion/initializer/database.rb +49 -0
- data/lib/adhearsion/initializer/drb.rb +31 -0
- data/lib/adhearsion/initializer/freeswitch.rb +22 -0
- data/lib/adhearsion/initializer/rails.rb +40 -0
- data/lib/adhearsion/initializer.rb +373 -0
- data/lib/adhearsion/logging.rb +92 -0
- data/lib/adhearsion/tasks/database.rb +5 -0
- data/lib/adhearsion/tasks/deprecations.rb +59 -0
- data/lib/adhearsion/tasks/generating.rb +20 -0
- data/lib/adhearsion/tasks/lint.rb +4 -0
- data/lib/adhearsion/tasks/testing.rb +37 -0
- data/lib/adhearsion/tasks.rb +16 -0
- data/lib/adhearsion/version.rb +9 -0
- data/lib/adhearsion/voip/asterisk/agi_server.rb +81 -0
- data/lib/adhearsion/voip/asterisk/commands.rb +1284 -0
- data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
- data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
- data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
- data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
- data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1754 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
- data/lib/adhearsion/voip/asterisk/manager_interface.rb +562 -0
- data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
- data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
- data/lib/adhearsion/voip/asterisk.rb +4 -0
- data/lib/adhearsion/voip/call.rb +440 -0
- data/lib/adhearsion/voip/call_routing.rb +64 -0
- data/lib/adhearsion/voip/commands.rb +9 -0
- data/lib/adhearsion/voip/constants.rb +39 -0
- data/lib/adhearsion/voip/conveniences.rb +18 -0
- data/lib/adhearsion/voip/dial_plan.rb +218 -0
- data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
- data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
- data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
- data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
- data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
- data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
- data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
- data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
- data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
- data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
- data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
- data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
- data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
- data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
- data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
- data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
- data/lib/adhearsion.rb +37 -0
- data/lib/theatre/README.markdown +64 -0
- data/lib/theatre/callback_definition_loader.rb +84 -0
- data/lib/theatre/guid.rb +23 -0
- data/lib/theatre/invocation.rb +121 -0
- data/lib/theatre/namespace_manager.rb +153 -0
- data/lib/theatre/version.rb +2 -0
- data/lib/theatre.rb +151 -0
- metadata +177 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
module Adhearsion
|
2
|
+
module VoIP
|
3
|
+
module DSL
|
4
|
+
# In Ruby, a number with a leading zero such as 023.45 or 023 does not evaluate as expected.
|
5
|
+
# In the case of the float 023.45, you get a syntax error. In the case of 023 the number is
|
6
|
+
# parsed as being in octal form and the number 19 is returned.
|
7
|
+
#
|
8
|
+
# In Adhearsion, various strings that are entirely numeric might start with zero and that zero
|
9
|
+
# should be preserved when converting that string of numbers into a number. The numerical string
|
10
|
+
# retains the zero while still allowing it to be compared to other numbers.
|
11
|
+
#
|
12
|
+
# [[I think this leading zero thing is only part of the reason that NumericalString exists. I'm
|
13
|
+
# currently writing tests for this leading zero stuff so I thought I'd dump some of my assumptions
|
14
|
+
# about it here in the documentation.]]
|
15
|
+
class NumericalString
|
16
|
+
class << self
|
17
|
+
def starts_with_leading_zero?(string)
|
18
|
+
string.to_s[/^0\d+/]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
(instance_methods - %w"__id__ __send__ __real_num __real_string").each do |m|
|
23
|
+
undef_method m
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :__real_num, :__real_string
|
27
|
+
|
28
|
+
def initialize(str)
|
29
|
+
@__real_string = str.to_s
|
30
|
+
@__real_num = str.to_i if @__real_string =~ /^\d+$/
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(name, *args, &block)
|
34
|
+
@__real_string.__send__ name, *args, &block
|
35
|
+
end
|
36
|
+
|
37
|
+
def respond_to?(m)
|
38
|
+
@__real_string.respond_to?(m) || m == :__real_num || m == :__real_string
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# The PhoneNumber class is used by one object throughout Adhearsion: the AGI
|
44
|
+
# "extension" variable. Using some clevery Ruby hackery, the Extension class allows
|
45
|
+
# dialplan writers to use the best of Fixnum and String usage such as
|
46
|
+
#
|
47
|
+
# - Dialing international numbers
|
48
|
+
# - Using a regexp in a case statement for "extension"
|
49
|
+
# - Using a numerical range against extension -- e.g. (100...200)
|
50
|
+
# - Using the thousands separator
|
51
|
+
class PhoneNumber < NumericalString
|
52
|
+
|
53
|
+
# Checks against a pattern identifying US local numbers (i.e numbers
|
54
|
+
# without an area code seven digits long)
|
55
|
+
def us_local_number?() to_s =~ Adhearsion::VoIP::Constants::US_LOCAL_NUMBER end
|
56
|
+
|
57
|
+
# Checks against a pattern identifying US domestic numbers.
|
58
|
+
def us_national_number?() to_s =~ Adhearsion::VoIP::Constants::US_NATIONAL_NUMBER end
|
59
|
+
|
60
|
+
# Checks against a pattern identifying an ISN number. See http://freenum.org
|
61
|
+
# for more info.
|
62
|
+
def isn?() to_s =~ Adhearsion::VoIP::Constants::ISN end
|
63
|
+
|
64
|
+
# Useful for dialing those 1-800-FUDGEME type numbers with letters in them. Letters
|
65
|
+
# in the argument will be converted to their appropriate keypad key.
|
66
|
+
def self.from_vanity str
|
67
|
+
str.gsub(/\W/, '').upcase.tr('A-Z', '22233344455566677778889999')
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# These monkey patches are necessary for the NumericalString to work, unfortunately.
|
77
|
+
class Class
|
78
|
+
def alias_method_once(new_name, old_name)
|
79
|
+
unless instance_methods.include?(new_name.to_s)
|
80
|
+
alias_method(new_name, old_name)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
[Object, Range, class << String; self; end].each do |klass|
|
86
|
+
klass.alias_method_once(:pre_modified_threequal, :===)
|
87
|
+
end
|
88
|
+
|
89
|
+
class Object
|
90
|
+
def ===(arg)
|
91
|
+
if arg.respond_to? :__real_string
|
92
|
+
arg = arg.__real_num if kind_of?(Numeric) || kind_of?(Range)
|
93
|
+
pre_modified_threequal arg
|
94
|
+
else
|
95
|
+
pre_modified_threequal arg
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Range
|
101
|
+
alias_method_once(:original_threequal, :===)
|
102
|
+
def ===(arg)
|
103
|
+
if (arg.respond_to? :__real_string) && !arg.__real_num.nil?
|
104
|
+
arg = arg.__real_num
|
105
|
+
original_threequal arg
|
106
|
+
else
|
107
|
+
original_threequal arg
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class << String
|
113
|
+
alias_method_once(:original_threequal, :===)
|
114
|
+
def ===(arg)
|
115
|
+
arg.respond_to?(:__real_string) || original_threequal(arg)
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Adhearsion
|
2
|
+
module VoIP
|
3
|
+
module FreeSwitch
|
4
|
+
|
5
|
+
class BasicConnectionManager
|
6
|
+
|
7
|
+
def initialize(io)
|
8
|
+
@io = io
|
9
|
+
end
|
10
|
+
|
11
|
+
# The send-command operator
|
12
|
+
def <<(str)
|
13
|
+
@io.write str + "\n\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_header
|
17
|
+
separate_pairs get_raw_header
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_raw_header
|
21
|
+
(returning [] do |lines|
|
22
|
+
until line = @io.gets and line.chomp.empty?
|
23
|
+
lines << line.chomp
|
24
|
+
end
|
25
|
+
end) * "\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
def next_event
|
29
|
+
header = get_raw_header
|
30
|
+
length = header.first[/\d+$/].to_i
|
31
|
+
# puts "Reading an event of #{length} bytes"
|
32
|
+
separate_pairs @io.read(length)
|
33
|
+
end
|
34
|
+
|
35
|
+
def separate_pairs(lines)
|
36
|
+
lines.inject({}) do |h,line|
|
37
|
+
returning h do |hash|
|
38
|
+
k,v = line.split(/\s*:\s*/)
|
39
|
+
hash[k] = URI.unescape(v).strip if k && v
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Adhearsion
|
2
|
+
module VoIP
|
3
|
+
module FreeSwitch
|
4
|
+
|
5
|
+
# Subclass this to register a new event handler
|
6
|
+
class EventHandler
|
7
|
+
|
8
|
+
@@events = {}
|
9
|
+
@@compound_events = {}
|
10
|
+
|
11
|
+
@@connection = nil
|
12
|
+
|
13
|
+
def self.start!(hash=nil)
|
14
|
+
login hash if hash
|
15
|
+
raise "You must login to the FreeSWITCH EventSocket!" unless @@connection
|
16
|
+
loop do
|
17
|
+
# debug "Waiting for an event"
|
18
|
+
dispatch_event! @@connection.get_header
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.dispatch_event!(data)
|
23
|
+
#puts "\nHandling an event! #{data.inspect}"
|
24
|
+
name = data['Event-Name']
|
25
|
+
normal_event = name && @@events[name.underscore.to_sym]
|
26
|
+
# puts "THIS IS WHAT I THINK IT MIGHT BE : #{normal_event.inspect} (with #{name.underscore.to_sym.inspect})"
|
27
|
+
if normal_event then normal_event.call(data)
|
28
|
+
else
|
29
|
+
#debug "Trying compound events"
|
30
|
+
@@compound_events.each do |(event, block)|
|
31
|
+
mini_event = {}
|
32
|
+
event.keys.each { |k| mini_event[k] = data[k] }
|
33
|
+
block.call(data) if event == mini_event
|
34
|
+
end
|
35
|
+
end
|
36
|
+
rescue => e
|
37
|
+
p e
|
38
|
+
puts e.backtrace.map { |x| " " * 4 + x }
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
# Can be specified in the subclass
|
44
|
+
def self.login(hash)
|
45
|
+
debug "Creating a new event connection manager"
|
46
|
+
@@connection = InboundConnectionManager.new hash
|
47
|
+
debug "Enabling events"
|
48
|
+
@@connection.enable_events!
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.on(event, &block)
|
52
|
+
event = event.underscore.to_sym if event.is_a? String
|
53
|
+
(event.kind_of?(Hash) ? @@compound_events : @@events)[event] = block
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'adhearsion/voip/dsl/dialplan/dispatcher'
|
2
|
+
|
3
|
+
module Adhearsion
|
4
|
+
module VoIP
|
5
|
+
module FreeSwitch
|
6
|
+
class FreeSwitchDialplanCommandFactory
|
7
|
+
|
8
|
+
def initialize(context=nil)
|
9
|
+
@context = context
|
10
|
+
end
|
11
|
+
|
12
|
+
# These should all return those objects...
|
13
|
+
def speak(text, hash={})
|
14
|
+
voice, engine = hash[:voice] || "Dianne", hash[:engine] || "cepstral"
|
15
|
+
|
16
|
+
dtmf = hash[:on_keypress]
|
17
|
+
speak_cmd = cmd 'speak', "#{engine}|#{voice}|%p" % text, :on_keypress => dtmf
|
18
|
+
|
19
|
+
if hash[:timeout] == 0
|
20
|
+
[speak_cmd, DSL::Dialplan::NoOpEventCommand.new(hash[:timeout], :on_keypress => dtmf)]
|
21
|
+
else
|
22
|
+
puts "Returning the normal speak command"
|
23
|
+
speak_cmd
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set(key, value)
|
28
|
+
cmd 'set', "#{key}=#{value}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def play(*files)
|
32
|
+
hash = files.last.kind_of?(Hash) ? files.pop : {}
|
33
|
+
conference, to = hash[:conference], hash[:to]
|
34
|
+
puts "conference: #{conference.inspect}, to: #{to.inspect}, hash: #{hash.inspect}, files: #{files.inspect}"
|
35
|
+
if conference
|
36
|
+
# Normal (inbound) event socket playing to a conference
|
37
|
+
files.map do |file|
|
38
|
+
cmd "conference", "#{conference} play #{file} #{to}"
|
39
|
+
end
|
40
|
+
elsif to
|
41
|
+
# Normal event socket syntax
|
42
|
+
files.map do |file|
|
43
|
+
# TODO: Support playing to an individual leg of the call.
|
44
|
+
cmd "broadcast", "#{to} #{file} both"
|
45
|
+
end
|
46
|
+
else
|
47
|
+
# Outbound event sockets
|
48
|
+
files.map { |file| cmd('playback', file) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def join(id)
|
53
|
+
DSL::Dialplan::ExitingEventCommand.new "conference", id.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def hangup!
|
57
|
+
cmd "exit"
|
58
|
+
end
|
59
|
+
|
60
|
+
def return!(obj)
|
61
|
+
raise DSL::Dialplan::ReturnValue.new(obj)
|
62
|
+
end
|
63
|
+
|
64
|
+
def hangup!
|
65
|
+
raise DSL::Dialplan::Hangup
|
66
|
+
end
|
67
|
+
|
68
|
+
def wait(seconds=nil, &block)
|
69
|
+
DSL::Dialplan::NoOpEventCommand.new(seconds)
|
70
|
+
end
|
71
|
+
|
72
|
+
def record(hash={}, &block)
|
73
|
+
# TODO: Could want to record a conference or a UUID
|
74
|
+
p hash
|
75
|
+
if hash[:stop]
|
76
|
+
cmd 'stop_record_session', hash[:stop]
|
77
|
+
else
|
78
|
+
file = hash[:file] || File.join(Dir::tmpdir, String.random(32), '.wav')
|
79
|
+
|
80
|
+
raise "Cannot supply both a timeout and a block!" if hash[:timeout] && block_given?
|
81
|
+
|
82
|
+
dtmf_breaker = lambda do |digit|
|
83
|
+
return! file if digit == hash[:break_on]
|
84
|
+
end
|
85
|
+
|
86
|
+
rec_cmd = cmd "record", file, :on_keypress => dtmf_breaker
|
87
|
+
returning [] do |cmds|
|
88
|
+
cmds << play('beep') if hash[:beep]
|
89
|
+
cmds << rec_cmd
|
90
|
+
if hash[:timeout]
|
91
|
+
cmds << DSL::Dialplan::NoOpEventCommand.new(hash[:timeout])
|
92
|
+
elsif block_given?
|
93
|
+
cmds << block
|
94
|
+
cmds << record(:stop => file)
|
95
|
+
end
|
96
|
+
cmds << file
|
97
|
+
p cmds
|
98
|
+
cmds
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def input(number=nil, hash={})
|
104
|
+
timeout, file = hash[:timeout], hash[:play] || hash[:file]
|
105
|
+
break_on = hash[:break_on] || '#'
|
106
|
+
|
107
|
+
# TODO: compile play() and set its DTMF callback to this one
|
108
|
+
digits = []
|
109
|
+
dtmf_hook = lambda do |digit|
|
110
|
+
puts "RECEIVED #{digit} WITH #{digits}"
|
111
|
+
return! digits.to_s if digit.to_s == break_on.to_s
|
112
|
+
digits << digit
|
113
|
+
return! digits.to_s if number && digits.size >= number
|
114
|
+
end
|
115
|
+
returning DSL::Dialplan::NoOpEventCommand.new do |command|
|
116
|
+
command.on_keypress &dtmf_hook
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def cmd(*args, &block)
|
123
|
+
DSL::Dialplan::EventCommand.new(*args, &block)
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'adhearsion/voip/freeswitch/basic_connection_manager'
|
2
|
+
module Adhearsion
|
3
|
+
module VoIP
|
4
|
+
module FreeSwitch
|
5
|
+
class InboundConnectionManager < BasicConnectionManager
|
6
|
+
|
7
|
+
DEFAULTS = { :pass => "ClueCon", :host => '127.0.0.1', :port => 8021 }
|
8
|
+
|
9
|
+
def initialize(arg)
|
10
|
+
if arg.kind_of? Hash
|
11
|
+
@opts = DEFAULTS.merge arg
|
12
|
+
@io = TCPSocket.new(@opts[:host], @opts[:port])
|
13
|
+
super @io
|
14
|
+
unless login(@opts[:pass])
|
15
|
+
raise "Your FreeSwitch Event Socket password for #{@opts[:host]} was invalid!"
|
16
|
+
end
|
17
|
+
else arg.kind_of? IO
|
18
|
+
@io = arg
|
19
|
+
super @io
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def enable_events!(which='ALL')
|
24
|
+
self << "event plain #{which}"
|
25
|
+
get_raw_header
|
26
|
+
end
|
27
|
+
|
28
|
+
# Only called when nothing has been sent over the socket.
|
29
|
+
def login(pass)
|
30
|
+
get_raw_header
|
31
|
+
self << "auth #{pass}"
|
32
|
+
get_raw_header.include? "+OK"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'gserver'
|
2
|
+
|
3
|
+
require 'adhearsion/voip/dsl/dialplan/thread_mixin'
|
4
|
+
require 'adhearsion/voip/dsl/dialplan/parser'
|
5
|
+
require 'adhearsion/voip/dsl/dialplan/dispatcher'
|
6
|
+
require 'adhearsion/voip/freeswitch/basic_connection_manager'
|
7
|
+
require 'adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory'
|
8
|
+
|
9
|
+
module Adhearsion
|
10
|
+
module VoIP
|
11
|
+
module FreeSwitch
|
12
|
+
class OesServer < GServer
|
13
|
+
|
14
|
+
def initialize(port, host=nil)
|
15
|
+
@port, @host = port || 4572, host || "0.0.0.0"
|
16
|
+
@cache_lock = Mutex.new
|
17
|
+
super @port, @host, (1.0/0.0)
|
18
|
+
log "Starting FreeSwitch OES Server"
|
19
|
+
end
|
20
|
+
|
21
|
+
def serve(io)
|
22
|
+
|
23
|
+
log "Incoming call on the FreeSwitch outbound event socket..."
|
24
|
+
Thread.me.extend DSL::Dialplan::ThreadMixin
|
25
|
+
Thread.me.call.io = io
|
26
|
+
conn = BasicConnectionManager.new io
|
27
|
+
|
28
|
+
Thread.my.call.mgr = conn
|
29
|
+
conn << "connect"
|
30
|
+
@vars = conn.get_header
|
31
|
+
answered = @vars['variable_endpoint_disposition'] == "ANSWER"
|
32
|
+
|
33
|
+
conn << "myevents"
|
34
|
+
myevents_response = conn.get_header
|
35
|
+
answered ||= myevents_response['Event-Name'] == 'CHANNEL_ANSWER'
|
36
|
+
|
37
|
+
log "Connected to Freeswitch. Waiting for answer state."
|
38
|
+
|
39
|
+
until answered
|
40
|
+
answered ||= conn.get_header['Event-Name'] == 'CHANNEL_ANSWER'
|
41
|
+
end
|
42
|
+
|
43
|
+
log "Loading cached dialplan"
|
44
|
+
contexts, dispatcher = cached_dialplan_data
|
45
|
+
log "Finished loading cached dialplans"
|
46
|
+
|
47
|
+
first_context_name = @vars['variable_context'] || @vars["Channel-Context"]
|
48
|
+
first_context = contexts[first_context_name.to_sym]
|
49
|
+
|
50
|
+
log "Found context #{first_context_name} from call variables."
|
51
|
+
|
52
|
+
# If the target context does not exist, warn and don't handle the call
|
53
|
+
unless first_context
|
54
|
+
log "No context '#{first_context_name}' found in " +
|
55
|
+
"#{AHN_CONFIG.files_from_setting("paths", "").to_sentence(:connector => "or")}. Ignoring request!"
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
# Enable events
|
60
|
+
|
61
|
+
# Now that we have the code, let's dispatch it back.
|
62
|
+
|
63
|
+
pretty_vars = rubyize_keys_for @vars
|
64
|
+
dispatcher.def_keys! pretty_vars
|
65
|
+
dispatcher.instance_eval(&first_context.block)
|
66
|
+
|
67
|
+
rescue => e
|
68
|
+
p e
|
69
|
+
puts e.backtrace.map {|x| " " * 4 + x }
|
70
|
+
end
|
71
|
+
|
72
|
+
def cached_dialplan_data
|
73
|
+
@cache_lock.synchronize do
|
74
|
+
log "Checking whether the contexts should be reloaded"
|
75
|
+
if should_reload_contexts?
|
76
|
+
log "Getting the contexts"
|
77
|
+
@abstract_contexts = DSL::Dialplan::DialplanParser.get_contexts
|
78
|
+
log "Creating a new OesDispatcher"
|
79
|
+
@abstract_dispatcher = OesDispatcher.new @vars['Channel-Unique-ID']
|
80
|
+
log "Done creating it"
|
81
|
+
@abstract_dispatcher.def_keys! @abstract_contexts
|
82
|
+
else
|
83
|
+
log "Should not reload context."
|
84
|
+
@abstract_dispatcher.instance_variable_set :@uuid, @vars['Channel-Unique-ID']
|
85
|
+
end
|
86
|
+
return [@abstract_contexts.clone, @abstract_dispatcher.clone]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# TODO. This is broken. Always returns true. Should cache the last reload
|
91
|
+
# time.
|
92
|
+
def should_reload_contexts?
|
93
|
+
!@abstract_contexts || !@abstract_dispatcher ||
|
94
|
+
AHN_CONFIG.files_from_setting("paths", "dialplan").map { |x| File.mtime(x) }.max < Time.now
|
95
|
+
end
|
96
|
+
|
97
|
+
def rubyize_keys_for(hash)
|
98
|
+
returning({}) do |pretty|
|
99
|
+
hash.each { |k,v| pretty[k.to_s.underscore] = v }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class OesDispatcher < DSL::Dialplan::CommandDispatcher
|
104
|
+
|
105
|
+
def initialize(uuid=nil)
|
106
|
+
super FreeSwitchDialplanCommandFactory, uuid
|
107
|
+
end
|
108
|
+
|
109
|
+
def dispatch!(event)
|
110
|
+
if event.kind_of?(DSL::Dialplan::NoOpEventCommand) && event.on_keypress
|
111
|
+
return_value = nil
|
112
|
+
dispatch = lambda do
|
113
|
+
loop do
|
114
|
+
Thread.my.call.mgr.get_raw_header
|
115
|
+
async_event = Thread.my.call.mgr.get_header
|
116
|
+
if async_event['Event-Name'] == 'DTMF'
|
117
|
+
key = async_event['DTMF-String']
|
118
|
+
return_value = event.on_keypress.call(('0'..'9').include?(key) ? key.to_i : key)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
if event.timeout
|
123
|
+
begin
|
124
|
+
Timeout.timeout event.timeout, &dispatch
|
125
|
+
rescue Timeout::Error
|
126
|
+
break!
|
127
|
+
return_value
|
128
|
+
end
|
129
|
+
else dispatch.call
|
130
|
+
end
|
131
|
+
|
132
|
+
else
|
133
|
+
log "Not a noop. Sending #{event.app}(#{event.args.to_a * " "})"
|
134
|
+
Thread.my.call.mgr << "SendMsg\ncall-command: execute\nexecute-app-name: " +
|
135
|
+
"#{event.app}\nexecute-app-arg: #{event.args.to_a * " "}"
|
136
|
+
|
137
|
+
if event.kind_of? DSL::Dialplan::ExitingEventCommand
|
138
|
+
Thread.my.call.io.close
|
139
|
+
Thread.me.exit
|
140
|
+
end
|
141
|
+
|
142
|
+
# Useless "command/reply" +OK and content-length headers
|
143
|
+
lambda do
|
144
|
+
Thread.my.call.mgr.get_raw_header
|
145
|
+
redo if Thread.my.call.mgr.get_header['Event-Name'] == "CHANNEL_EXECUTE_COMPLETE"
|
146
|
+
end.call
|
147
|
+
|
148
|
+
# Main event information. Keep track of the Core-UUID and wait for
|
149
|
+
# it to come back to us as a CHANNEL_EXECUTE_COMPLETE event.
|
150
|
+
execution_header = Thread.my.call.mgr.get_header
|
151
|
+
execution_uuid = execution_header['Core-UUID']
|
152
|
+
|
153
|
+
loop do
|
154
|
+
log "Waiting for either a DTMF or the app to finish"
|
155
|
+
hdr = Thread.my.call.mgr.get_raw_header
|
156
|
+
log "Got head #{hdr}"
|
157
|
+
|
158
|
+
if hdr == "Content-Type: api/response\nContent-Length: 0"
|
159
|
+
break
|
160
|
+
end
|
161
|
+
|
162
|
+
async_event = Thread.my.call.mgr.get_header
|
163
|
+
event_name = async_event['Event-Name']
|
164
|
+
if event_name == 'DTMF' && event.on_keypress
|
165
|
+
key = async_event['DTMF-String']
|
166
|
+
event.on_keypress.call(('0'..'9') === key ? key.to_i : key)
|
167
|
+
elsif event_name == 'CHANNEL_EXECUTE_COMPLETE' && async_event['Core-UUID'] == execution_uuid
|
168
|
+
break async_event
|
169
|
+
else
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
rescue DSL::Dialplan::ReturnValue => r
|
175
|
+
log "Dispatch!: Got a return value with #{r.obj}"
|
176
|
+
break!
|
177
|
+
raise r
|
178
|
+
rescue DSL::Dialplan::Hangup
|
179
|
+
Thread.my.call.mgr << "SendMsg\ncall-command: hangup"
|
180
|
+
Thread.my.call.mgr.io.close rescue nil
|
181
|
+
end
|
182
|
+
|
183
|
+
def break!(uuid=@context)
|
184
|
+
log "Breaking with #{uuid}"
|
185
|
+
Thread.my.call.mgr << "api break #{uuid}"
|
186
|
+
Thread.my.call.mgr.get_raw_header
|
187
|
+
# Thread.my.call.mgr.get_raw_header
|
188
|
+
# Thread.my.call.mgr.get_raw_header
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Adhearsion
|
2
|
+
module VoIP
|
3
|
+
class CalculatedMatch
|
4
|
+
|
5
|
+
# Convenience method for instantiating failed matches
|
6
|
+
def self.failed_match!(pattern, query, match_payload)
|
7
|
+
new :pattern => pattern, :query => query, :match_payload => match_payload
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :match_payload, :potential_matches, :exact_matches, :pattern, :query
|
11
|
+
|
12
|
+
def initialize(options={})
|
13
|
+
@pattern, @query, @match_payload = options.values_at :pattern, :query, :match_payload
|
14
|
+
@potential_matches = options[:potential_matches] ? Array(options[:potential_matches]) : []
|
15
|
+
@exact_matches = options[:exact_matches] ? Array(options[:exact_matches]) : []
|
16
|
+
end
|
17
|
+
|
18
|
+
def exact_match?
|
19
|
+
exact_matches.any?
|
20
|
+
end
|
21
|
+
|
22
|
+
def potential_match?
|
23
|
+
potential_matches.any?
|
24
|
+
end
|
25
|
+
|
26
|
+
def failed_match?
|
27
|
+
!potential_match? && !exact_match?
|
28
|
+
end
|
29
|
+
|
30
|
+
def type_of_match
|
31
|
+
if exact_match?
|
32
|
+
:exact
|
33
|
+
elsif potential_match?
|
34
|
+
:potential
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
class CalculatedMatchCollection
|
41
|
+
|
42
|
+
attr_reader :calculated_matches, :potential_matches, :exact_matches,
|
43
|
+
:actual_potential_matches, :actual_exact_matches
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@calculated_matches = []
|
47
|
+
@potential_matches = []
|
48
|
+
@exact_matches = []
|
49
|
+
@actual_potential_matches = []
|
50
|
+
@actual_exact_matches = []
|
51
|
+
end
|
52
|
+
|
53
|
+
def <<(calculated_match)
|
54
|
+
calculated_matches << calculated_match
|
55
|
+
actual_potential_matches.concat calculated_match.potential_matches
|
56
|
+
actual_exact_matches.concat calculated_match.exact_matches
|
57
|
+
|
58
|
+
potential_matches << calculated_match if calculated_match.potential_match?
|
59
|
+
exact_matches << calculated_match if calculated_match.exact_match?
|
60
|
+
end
|
61
|
+
|
62
|
+
def potential_match_count
|
63
|
+
actual_potential_matches.size
|
64
|
+
end
|
65
|
+
|
66
|
+
def exact_match_count
|
67
|
+
actual_exact_matches.size
|
68
|
+
end
|
69
|
+
|
70
|
+
def potential_match?
|
71
|
+
potential_match_count > 0
|
72
|
+
end
|
73
|
+
|
74
|
+
def exact_match?
|
75
|
+
exact_match_count > 0
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|