punchblock 0.8.4 → 0.9.0
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/.travis.yml +9 -0
- data/CHANGELOG.md +5 -0
- data/Rakefile +0 -6
- data/lib/punchblock.rb +0 -6
- data/lib/punchblock/command/dial.rb +1 -0
- data/lib/punchblock/connection/xmpp.rb +2 -1
- data/lib/punchblock/translator/asterisk/call.rb +12 -10
- data/lib/punchblock/translator/asterisk/component.rb +2 -0
- data/lib/punchblock/translator/asterisk/component/asterisk.rb +0 -2
- data/lib/punchblock/translator/asterisk/component/input.rb +114 -0
- data/lib/punchblock/translator/asterisk/component/output.rb +92 -0
- data/lib/punchblock/version.rb +1 -1
- data/punchblock.gemspec +0 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +29 -3
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +282 -0
- data/spec/punchblock/translator/asterisk/component/output_spec.rb +491 -0
- metadata +46 -60
- data/lib/punchblock/component/tropo.rb +0 -9
- data/lib/punchblock/component/tropo/conference.rb +0 -331
- data/lib/punchblock/translator/asterisk/component/asterisk/input.rb +0 -116
- data/lib/punchblock/translator/asterisk/component/asterisk/output.rb +0 -94
- data/spec/punchblock/component/tropo/conference_spec.rb +0 -361
- data/spec/punchblock/translator/asterisk/component/asterisk/input_spec.rb +0 -284
- data/spec/punchblock/translator/asterisk/component/asterisk/output_spec.rb +0 -493
data/.travis.yml
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# develop
|
|
2
2
|
|
|
3
|
+
# v0.9.0 - 2012-01-30
|
|
4
|
+
* Bugfix: Remove the rest of the deprecated Tropo components (conference)
|
|
5
|
+
* Feature: Outbound dials on Asterisk now respect the dial timeout
|
|
6
|
+
* Bugfix: Registering stanza handlers on an XMPP connection now sets them in the correct order such that they do not override the internally defined handlers
|
|
7
|
+
|
|
3
8
|
# v0.8.4 - 2012-01-19
|
|
4
9
|
* Bugfix: End, Ringing & Answered events are allowed to have headers
|
|
5
10
|
* Feature: Dial commands may have an optional timeout
|
data/Rakefile
CHANGED
|
@@ -10,12 +10,6 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
|
10
10
|
spec.rspec_opts = '--color'
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
|
14
|
-
spec.pattern = 'spec/**/*_spec.rb'
|
|
15
|
-
spec.rcov = true
|
|
16
|
-
spec.rspec_opts = '--color'
|
|
17
|
-
end
|
|
18
|
-
|
|
19
13
|
task :default => :spec
|
|
20
14
|
task :ci => ['ci:setup:rspec', :spec]
|
|
21
15
|
task :hudson => :ci
|
data/lib/punchblock.rb
CHANGED
|
@@ -44,7 +44,6 @@ module Punchblock
|
|
|
44
44
|
TransportError = Class.new StandardError
|
|
45
45
|
|
|
46
46
|
BASE_RAYO_NAMESPACE = 'urn:xmpp:rayo'
|
|
47
|
-
BASE_TROPO_NAMESPACE = 'urn:xmpp:tropo'
|
|
48
47
|
BASE_ASTERISK_NAMESPACE = 'urn:xmpp:rayo:asterisk'
|
|
49
48
|
RAYO_VERSION = '1'
|
|
50
49
|
RAYO_NAMESPACES = {:core => [BASE_RAYO_NAMESPACE, RAYO_VERSION].compact.join(':')}
|
|
@@ -54,11 +53,6 @@ module Punchblock
|
|
|
54
53
|
RAYO_NAMESPACES[:"#{ns}_complete"] = [BASE_RAYO_NAMESPACE, ns.to_s, 'complete', RAYO_VERSION].compact.join(':')
|
|
55
54
|
end
|
|
56
55
|
|
|
57
|
-
[:conference].each do |ns|
|
|
58
|
-
RAYO_NAMESPACES[ns] = [BASE_TROPO_NAMESPACE, ns.to_s, RAYO_VERSION].compact.join(':')
|
|
59
|
-
RAYO_NAMESPACES[:"#{ns}_complete"] = [BASE_TROPO_NAMESPACE, ns.to_s, 'complete', RAYO_VERSION].compact.join(':')
|
|
60
|
-
end
|
|
61
|
-
|
|
62
56
|
[:agi, :ami].each do |ns|
|
|
63
57
|
RAYO_NAMESPACES[ns] = [BASE_ASTERISK_NAMESPACE, ns.to_s, RAYO_VERSION].compact.join(':')
|
|
64
58
|
RAYO_NAMESPACES[:"#{ns}_complete"] = [BASE_ASTERISK_NAMESPACE, ns.to_s, 'complete', RAYO_VERSION].compact.join(':')
|
|
@@ -11,6 +11,7 @@ module Punchblock
|
|
|
11
11
|
# @param [Hash] options
|
|
12
12
|
# @option options [String] :to destination to dial
|
|
13
13
|
# @option options [String, Optional] :from what to set the Caller ID to
|
|
14
|
+
# @option options [Integer, Optional] :timeout in milliseconds
|
|
14
15
|
# @option options [Array[Header], Hash, Optional] :headers SIP headers to attach to
|
|
15
16
|
# the new call. Can be either a hash of key-value pairs, or an array of
|
|
16
17
|
# Header objects.
|
|
@@ -38,6 +38,8 @@ module Punchblock
|
|
|
38
38
|
Blather.logger = pb_logger
|
|
39
39
|
Blather.default_log_level = :trace if Blather.respond_to? :default_log_level
|
|
40
40
|
|
|
41
|
+
register_handlers
|
|
42
|
+
|
|
41
43
|
super()
|
|
42
44
|
end
|
|
43
45
|
|
|
@@ -68,7 +70,6 @@ module Punchblock
|
|
|
68
70
|
# Fire up the connection
|
|
69
71
|
#
|
|
70
72
|
def run
|
|
71
|
-
register_handlers
|
|
72
73
|
connect
|
|
73
74
|
end
|
|
74
75
|
|
|
@@ -49,15 +49,17 @@ module Punchblock
|
|
|
49
49
|
|
|
50
50
|
def dial(dial_command)
|
|
51
51
|
@direction = :outbound
|
|
52
|
+
params = { :async => true,
|
|
53
|
+
:application => 'AGI',
|
|
54
|
+
:data => 'agi:async',
|
|
55
|
+
:channel => dial_command.to,
|
|
56
|
+
:callerid => dial_command.from,
|
|
57
|
+
:variable => "punchblock_call_id=#{id}"
|
|
58
|
+
}
|
|
59
|
+
params[:timeout] = dial_command.timeout unless dial_command.timeout.nil?
|
|
60
|
+
|
|
52
61
|
originate_action = Punchblock::Component::Asterisk::AMI::Action.new :name => 'Originate',
|
|
53
|
-
:params =>
|
|
54
|
-
:async => true,
|
|
55
|
-
:application => 'AGI',
|
|
56
|
-
:data => 'agi:async',
|
|
57
|
-
:channel => dial_command.to,
|
|
58
|
-
:callerid => dial_command.from,
|
|
59
|
-
:variable => "punchblock_call_id=#{id}"
|
|
60
|
-
}
|
|
62
|
+
:params => params
|
|
61
63
|
originate_action.request!
|
|
62
64
|
translator.execute_global_command! originate_action
|
|
63
65
|
dial_command.response = Ref.new :id => id
|
|
@@ -133,9 +135,9 @@ module Punchblock
|
|
|
133
135
|
when Punchblock::Component::Asterisk::AGI::Command
|
|
134
136
|
execute_component Component::Asterisk::AGICommand, command
|
|
135
137
|
when Punchblock::Component::Output
|
|
136
|
-
execute_component Component::
|
|
138
|
+
execute_component Component::Output, command
|
|
137
139
|
when Punchblock::Component::Input
|
|
138
|
-
execute_component Component::
|
|
140
|
+
execute_component Component::Input, command
|
|
139
141
|
else
|
|
140
142
|
command.response = ProtocolError.new 'command-not-acceptable', "Did not understand command for call #{id}", id
|
|
141
143
|
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module Punchblock
|
|
2
|
+
module Translator
|
|
3
|
+
class Asterisk
|
|
4
|
+
module Component
|
|
5
|
+
class Input < Component
|
|
6
|
+
|
|
7
|
+
attr_reader :grammar, :buffer
|
|
8
|
+
|
|
9
|
+
def setup
|
|
10
|
+
@media_engine = call.translator.media_engine
|
|
11
|
+
@buffer = ""
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute
|
|
15
|
+
initial_timeout = @component_node.initial_timeout || -1
|
|
16
|
+
@inter_digit_timeout = @component_node.inter_digit_timeout || -1
|
|
17
|
+
|
|
18
|
+
return with_error 'option error', 'A grammar document is required.' unless @component_node.grammar
|
|
19
|
+
return with_error 'option error', 'A mode value other than DTMF is unsupported on Asterisk.' unless @component_node.mode == :dtmf
|
|
20
|
+
return with_error 'option error', 'An initial timeout value that is negative (and not -1) is invalid.' unless initial_timeout >= -1
|
|
21
|
+
return with_error 'option error', 'An inter-digit timeout value that is negative (and not -1) is invalid.' unless @inter_digit_timeout >= -1
|
|
22
|
+
|
|
23
|
+
send_ref
|
|
24
|
+
|
|
25
|
+
case @media_engine
|
|
26
|
+
when :asterisk, nil
|
|
27
|
+
@grammar = @component_node.grammar.value.clone
|
|
28
|
+
grammar.inline!
|
|
29
|
+
grammar.tokenize!
|
|
30
|
+
grammar.normalize_whitespace
|
|
31
|
+
|
|
32
|
+
begin_initial_timer initial_timeout/1000 unless initial_timeout == -1
|
|
33
|
+
|
|
34
|
+
component = current_actor
|
|
35
|
+
|
|
36
|
+
@active = true
|
|
37
|
+
|
|
38
|
+
call.register_handler :ami, :name => 'DTMF' do |event|
|
|
39
|
+
component.process_dtmf! event['Digit'] if event['End'] == 'Yes'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def process_dtmf(digit)
|
|
45
|
+
return unless @active
|
|
46
|
+
pb_logger.trace "Processing incoming DTMF digit #{digit}"
|
|
47
|
+
buffer << digit
|
|
48
|
+
cancel_initial_timer
|
|
49
|
+
case (match = grammar.match buffer.dup)
|
|
50
|
+
when RubySpeech::GRXML::Match
|
|
51
|
+
pb_logger.trace "Found a match against buffer #{buffer}"
|
|
52
|
+
complete success_reason(match)
|
|
53
|
+
when RubySpeech::GRXML::NoMatch
|
|
54
|
+
pb_logger.trace "Buffer #{buffer} does not match grammar"
|
|
55
|
+
complete Punchblock::Component::Input::Complete::NoMatch.new
|
|
56
|
+
when RubySpeech::GRXML::PotentialMatch
|
|
57
|
+
pb_logger.trace "Buffer #{buffer} potentially matches grammar. Waiting..."
|
|
58
|
+
reset_inter_digit_timer
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def begin_initial_timer(timeout)
|
|
65
|
+
pb_logger.trace "Setting initial timer for #{timeout} seconds"
|
|
66
|
+
@initial_timer = after timeout do
|
|
67
|
+
pb_logger.trace "Initial timer expired."
|
|
68
|
+
complete Punchblock::Component::Input::Complete::NoInput.new
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cancel_initial_timer
|
|
73
|
+
return unless @initial_timer
|
|
74
|
+
@initial_timer.cancel
|
|
75
|
+
@initial_timer = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def reset_inter_digit_timer
|
|
79
|
+
return if @inter_digit_timeout == -1
|
|
80
|
+
@inter_digit_timer ||= begin
|
|
81
|
+
pb_logger.trace "Setting inter-digit timer for #{@inter_digit_timeout/1000} seconds"
|
|
82
|
+
after @inter_digit_timeout/1000 do
|
|
83
|
+
pb_logger.trace "Inter digit-timer expired."
|
|
84
|
+
complete Punchblock::Component::Input::Complete::NoMatch.new
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
pb_logger.trace "Resetting inter-digit timer"
|
|
88
|
+
@inter_digit_timer.reset
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def cancel_inter_digit_timer
|
|
92
|
+
return unless @inter_digit_timer
|
|
93
|
+
@inter_digit_timer.cancel
|
|
94
|
+
@inter_digit_timer = nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def success_reason(match)
|
|
98
|
+
Punchblock::Component::Input::Complete::Success.new :mode => match.mode,
|
|
99
|
+
:confidence => match.confidence,
|
|
100
|
+
:utterance => match.utterance,
|
|
101
|
+
:interpretation => match.interpretation
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def complete(reason)
|
|
105
|
+
@active = false
|
|
106
|
+
cancel_initial_timer
|
|
107
|
+
cancel_inter_digit_timer
|
|
108
|
+
send_complete_event reason
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require 'active_support/core_ext/string/filters'
|
|
2
|
+
|
|
3
|
+
module Punchblock
|
|
4
|
+
module Translator
|
|
5
|
+
class Asterisk
|
|
6
|
+
module Component
|
|
7
|
+
class Output < Component
|
|
8
|
+
|
|
9
|
+
def setup
|
|
10
|
+
@media_engine = @call.translator.media_engine
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def execute
|
|
14
|
+
return with_error 'option error', 'An SSML document is required.' unless @component_node.ssml
|
|
15
|
+
|
|
16
|
+
return with_error 'option error', 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :speech
|
|
17
|
+
|
|
18
|
+
[:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
|
|
19
|
+
return with_error 'option error', "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
case @media_engine
|
|
23
|
+
when :asterisk, nil
|
|
24
|
+
return with_error 'option error', "A voice value is unsupported on Asterisk." if @component_node.voice
|
|
25
|
+
|
|
26
|
+
@execution_elements = @component_node.ssml.children.map do |node|
|
|
27
|
+
case node
|
|
28
|
+
when RubySpeech::SSML::Audio
|
|
29
|
+
lambda { current_actor.play_audio! node.src }
|
|
30
|
+
end
|
|
31
|
+
end.compact
|
|
32
|
+
|
|
33
|
+
@pending_actions = @execution_elements.count
|
|
34
|
+
|
|
35
|
+
send_ref
|
|
36
|
+
|
|
37
|
+
@interrupt_digits = '0123456789*#' if [:any, :dtmf].include? @component_node.interrupt_on
|
|
38
|
+
|
|
39
|
+
@execution_elements.each do |element|
|
|
40
|
+
element.call
|
|
41
|
+
wait :continue
|
|
42
|
+
process_playback_completion
|
|
43
|
+
end
|
|
44
|
+
when :unimrcp
|
|
45
|
+
doc = @component_node.ssml.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
|
|
46
|
+
send_ref
|
|
47
|
+
@call.send_agi_action! 'EXEC MRCPSynth', doc, mrcpsynth_options do |complete_event|
|
|
48
|
+
pb_logger.debug "MRCPSynth completed with #{complete_event}."
|
|
49
|
+
send_complete_event success_reason
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def process_playback_completion
|
|
55
|
+
@pending_actions -= 1
|
|
56
|
+
pb_logger.debug "Received action completion. Now waiting on #{@pending_actions} actions."
|
|
57
|
+
if @pending_actions < 1
|
|
58
|
+
pb_logger.debug "Sending complete event"
|
|
59
|
+
send_complete_event success_reason
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def continue(event = nil)
|
|
64
|
+
signal :continue, event
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def play_audio(path)
|
|
68
|
+
pb_logger.debug "Playing an audio file (#{path}) via STREAM FILE"
|
|
69
|
+
op = current_actor
|
|
70
|
+
@call.send_agi_action! 'STREAM FILE', path, @interrupt_digits do |complete_event|
|
|
71
|
+
pb_logger.debug "STREAM FILE completed with #{complete_event}. Signalling to continue execution."
|
|
72
|
+
op.continue! complete_event
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def mrcpsynth_options
|
|
79
|
+
[].tap do |opts|
|
|
80
|
+
opts << 'i=any' if [:any, :dtmf].include? @component_node.interrupt_on
|
|
81
|
+
opts << "v=#{@component_node.voice}" if @component_node.voice
|
|
82
|
+
end.join '&'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def success_reason
|
|
86
|
+
Punchblock::Component::Output::Complete::Success.new
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/punchblock/version.rb
CHANGED
data/punchblock.gemspec
CHANGED
|
@@ -36,7 +36,6 @@ Gem::Specification.new do |s|
|
|
|
36
36
|
s.add_development_dependency %q<rspec>, ["~> 2.7.0"]
|
|
37
37
|
s.add_development_dependency %q<ci_reporter>, [">= 1.6.3"]
|
|
38
38
|
s.add_development_dependency %q<yard>, ["~> 0.6.0"]
|
|
39
|
-
s.add_development_dependency %q<rcov>, [">= 0"]
|
|
40
39
|
s.add_development_dependency %q<rake>, [">= 0"]
|
|
41
40
|
s.add_development_dependency %q<mocha>, [">= 0"]
|
|
42
41
|
s.add_development_dependency %q<i18n>, [">= 0"]
|
|
@@ -67,6 +67,7 @@ module Punchblock
|
|
|
67
67
|
describe '#shutdown' do
|
|
68
68
|
it 'should terminate the actor' do
|
|
69
69
|
subject.shutdown
|
|
70
|
+
sleep 0.5
|
|
70
71
|
subject.should_not be_alive
|
|
71
72
|
end
|
|
72
73
|
end
|
|
@@ -99,8 +100,10 @@ module Punchblock
|
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
describe '#dial' do
|
|
103
|
+
let(:dial_command_options) { {} }
|
|
104
|
+
|
|
102
105
|
let :dial_command do
|
|
103
|
-
Punchblock::Command::Dial.new
|
|
106
|
+
Punchblock::Command::Dial.new({:to => 'SIP/1234', :from => 'sip:foo@bar.com'}.merge(dial_command_options))
|
|
104
107
|
end
|
|
105
108
|
|
|
106
109
|
before { dial_command.request! }
|
|
@@ -120,6 +123,28 @@ module Punchblock
|
|
|
120
123
|
subject.dial dial_command
|
|
121
124
|
end
|
|
122
125
|
|
|
126
|
+
context 'with a timeout specified' do
|
|
127
|
+
let :dial_command_options do
|
|
128
|
+
{ :timeout => 10000 }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'includes the timeout in the Originate AMI action' do
|
|
132
|
+
expected_action = Punchblock::Component::Asterisk::AMI::Action.new :name => 'Originate',
|
|
133
|
+
:params => {
|
|
134
|
+
:async => true,
|
|
135
|
+
:application => 'AGI',
|
|
136
|
+
:data => 'agi:async',
|
|
137
|
+
:channel => 'SIP/1234',
|
|
138
|
+
:callerid => 'sip:foo@bar.com',
|
|
139
|
+
:variable => "punchblock_call_id=#{subject.id}",
|
|
140
|
+
:timeout => 10000
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
translator.expects(:execute_global_command!).once.with expected_action
|
|
144
|
+
subject.dial dial_command
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
123
148
|
it 'sends the call ID as a response to the Dial' do
|
|
124
149
|
subject.dial dial_command
|
|
125
150
|
dial_command.response
|
|
@@ -162,6 +187,7 @@ module Punchblock
|
|
|
162
187
|
it "should cause the actor to be terminated" do
|
|
163
188
|
translator.expects(:handle_pb_event!).once
|
|
164
189
|
subject.process_ami_event ami_event
|
|
190
|
+
sleep 0.5
|
|
165
191
|
subject.should_not be_alive
|
|
166
192
|
end
|
|
167
193
|
|
|
@@ -442,7 +468,7 @@ module Punchblock
|
|
|
442
468
|
let(:mock_action) { mock 'Component::Asterisk::Output', :id => 'foo' }
|
|
443
469
|
|
|
444
470
|
it 'should create an AGI command component actor and execute it asynchronously' do
|
|
445
|
-
Component::
|
|
471
|
+
Component::Output.expects(:new).once.with(command, subject).returns mock_action
|
|
446
472
|
mock_action.expects(:internal=).never
|
|
447
473
|
mock_action.expects(:execute!).once
|
|
448
474
|
subject.execute_command command
|
|
@@ -457,7 +483,7 @@ module Punchblock
|
|
|
457
483
|
let(:mock_action) { mock 'Component::Asterisk::Input', :id => 'foo' }
|
|
458
484
|
|
|
459
485
|
it 'should create an AGI command component actor and execute it asynchronously' do
|
|
460
|
-
Component::
|
|
486
|
+
Component::Input.expects(:new).once.with(command, subject).returns mock_action
|
|
461
487
|
mock_action.expects(:internal=).never
|
|
462
488
|
mock_action.expects(:execute!).once
|
|
463
489
|
subject.execute_command command
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Punchblock
|
|
4
|
+
module Translator
|
|
5
|
+
class Asterisk
|
|
6
|
+
module Component
|
|
7
|
+
describe Input do
|
|
8
|
+
let(:connection) do
|
|
9
|
+
mock_connection_with_event_handler do |event|
|
|
10
|
+
command.add_event event
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
let(:media_engine) { nil }
|
|
14
|
+
let(:translator) { Punchblock::Translator::Asterisk.new mock('AMI'), connection, media_engine }
|
|
15
|
+
let(:call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator }
|
|
16
|
+
let(:command_options) { {} }
|
|
17
|
+
|
|
18
|
+
let :command do
|
|
19
|
+
Punchblock::Component::Input.new command_options
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let :grammar do
|
|
23
|
+
RubySpeech::GRXML.draw :mode => 'dtmf', :root => 'pin' do
|
|
24
|
+
rule id: 'digit' do
|
|
25
|
+
one_of do
|
|
26
|
+
0.upto(9) { |d| item { d.to_s } }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
rule id: 'pin', scope: 'public' do
|
|
31
|
+
item repeat: '2' do
|
|
32
|
+
ruleref uri: '#digit'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
subject { Input.new command, call }
|
|
39
|
+
|
|
40
|
+
describe '#execute' do
|
|
41
|
+
before { command.request! }
|
|
42
|
+
|
|
43
|
+
context 'with a media engine of :unimrcp' do
|
|
44
|
+
pending
|
|
45
|
+
let(:media_engine) { :unimrcp }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context 'with a media engine of :asterisk' do
|
|
49
|
+
let(:media_engine) { :asterisk }
|
|
50
|
+
|
|
51
|
+
let(:command_opts) { {} }
|
|
52
|
+
|
|
53
|
+
let :command_options do
|
|
54
|
+
{ :mode => :dtmf, :grammar => { :value => grammar } }.merge(command_opts)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ami_event_for_dtmf(digit, position)
|
|
58
|
+
RubyAMI::Event.new('DTMF').tap do |e|
|
|
59
|
+
e['Digit'] = digit.to_s
|
|
60
|
+
e['Start'] = position == :start ? 'Yes' : 'No'
|
|
61
|
+
e['End'] = position == :end ? 'Yes' : 'No'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def send_ami_events_for_dtmf(digit)
|
|
66
|
+
call.process_ami_event ami_event_for_dtmf(digit, :start)
|
|
67
|
+
call.process_ami_event ami_event_for_dtmf(digit, :end)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
let(:reason) { command.complete_event(5).reason }
|
|
71
|
+
|
|
72
|
+
describe "receiving DTMF events" do
|
|
73
|
+
before do
|
|
74
|
+
subject.execute
|
|
75
|
+
expected_event
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context "when a match is found" do
|
|
79
|
+
before do
|
|
80
|
+
send_ami_events_for_dtmf 1
|
|
81
|
+
send_ami_events_for_dtmf 2
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
let :expected_event do
|
|
85
|
+
Punchblock::Component::Input::Complete::Success.new :mode => :dtmf,
|
|
86
|
+
:confidence => 1,
|
|
87
|
+
:utterance => '12',
|
|
88
|
+
:interpretation => 'dtmf-1 dtmf-2',
|
|
89
|
+
:component_id => subject.id,
|
|
90
|
+
:call_id => call.id
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "should send a success complete event with the relevant data" do
|
|
94
|
+
reason.should == expected_event
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
context "when the match is invalid" do
|
|
99
|
+
before do
|
|
100
|
+
send_ami_events_for_dtmf 1
|
|
101
|
+
send_ami_events_for_dtmf '#'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
let :expected_event do
|
|
105
|
+
Punchblock::Component::Input::Complete::NoMatch.new :component_id => subject.id,
|
|
106
|
+
:call_id => call.id
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "should send a nomatch complete event" do
|
|
110
|
+
reason.should == expected_event
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe 'grammar' do
|
|
116
|
+
context 'unset' do
|
|
117
|
+
let(:command_opts) { { :grammar => nil } }
|
|
118
|
+
it "should return an error and not execute any actions" do
|
|
119
|
+
subject.execute
|
|
120
|
+
error = ProtocolError.new 'option error', 'A grammar document is required.'
|
|
121
|
+
command.response(0.1).should == error
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe 'mode' do
|
|
127
|
+
context 'unset' do
|
|
128
|
+
let(:command_opts) { { :mode => nil } }
|
|
129
|
+
it "should return an error and not execute any actions" do
|
|
130
|
+
subject.execute
|
|
131
|
+
error = ProtocolError.new 'option error', 'A mode value other than DTMF is unsupported on Asterisk.'
|
|
132
|
+
command.response(0.1).should == error
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
context 'any' do
|
|
137
|
+
let(:command_opts) { { :mode => :any } }
|
|
138
|
+
it "should return an error and not execute any actions" do
|
|
139
|
+
subject.execute
|
|
140
|
+
error = ProtocolError.new 'option error', 'A mode value other than DTMF is unsupported on Asterisk.'
|
|
141
|
+
command.response(0.1).should == error
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
context 'speech' do
|
|
146
|
+
let(:command_opts) { { :mode => :speech } }
|
|
147
|
+
it "should return an error and not execute any actions" do
|
|
148
|
+
subject.execute
|
|
149
|
+
error = ProtocolError.new 'option error', 'A mode value other than DTMF is unsupported on Asterisk.'
|
|
150
|
+
command.response(0.1).should == error
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe 'terminator' do
|
|
156
|
+
pending
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
describe 'recognizer' do
|
|
160
|
+
pending
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe 'initial-timeout' do
|
|
164
|
+
context 'a positive number' do
|
|
165
|
+
let(:command_opts) { { :initial_timeout => 1000 } }
|
|
166
|
+
|
|
167
|
+
it "should not cause a NoInput if first input is received in time" do
|
|
168
|
+
subject.execute
|
|
169
|
+
send_ami_events_for_dtmf 1
|
|
170
|
+
sleep 1.5
|
|
171
|
+
send_ami_events_for_dtmf 2
|
|
172
|
+
reason.should be_a Punchblock::Component::Input::Complete::Success
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "should cause a NoInput complete event to be sent after the timeout" do
|
|
176
|
+
subject.execute
|
|
177
|
+
sleep 1.5
|
|
178
|
+
send_ami_events_for_dtmf 1
|
|
179
|
+
send_ami_events_for_dtmf 2
|
|
180
|
+
reason.should be_a Punchblock::Component::Input::Complete::NoInput
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
context '-1' do
|
|
185
|
+
let(:command_opts) { { :initial_timeout => -1 } }
|
|
186
|
+
|
|
187
|
+
it "should not start a timer" do
|
|
188
|
+
subject.wrapped_object.expects(:begin_initial_timer).never
|
|
189
|
+
subject.execute
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
context 'unset' do
|
|
194
|
+
let(:command_opts) { { :initial_timeout => nil } }
|
|
195
|
+
|
|
196
|
+
it "should not start a timer" do
|
|
197
|
+
subject.wrapped_object.expects(:begin_initial_timer).never
|
|
198
|
+
subject.execute
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
context 'a negative number other than -1' do
|
|
203
|
+
let(:command_opts) { { :initial_timeout => -1000 } }
|
|
204
|
+
|
|
205
|
+
it "should return an error and not execute any actions" do
|
|
206
|
+
subject.execute
|
|
207
|
+
error = ProtocolError.new 'option error', 'An initial timeout value that is negative (and not -1) is invalid.'
|
|
208
|
+
command.response(0.1).should == error
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
describe 'inter-digit-timeout' do
|
|
214
|
+
context 'a positive number' do
|
|
215
|
+
let(:command_opts) { { :inter_digit_timeout => 1000 } }
|
|
216
|
+
|
|
217
|
+
it "should not prevent a Match if input is received in time" do
|
|
218
|
+
subject.execute
|
|
219
|
+
sleep 1.5
|
|
220
|
+
send_ami_events_for_dtmf 1
|
|
221
|
+
sleep 0.5
|
|
222
|
+
send_ami_events_for_dtmf 2
|
|
223
|
+
reason.should be_a Punchblock::Component::Input::Complete::Success
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "should cause a NoMatch complete event to be sent after the timeout" do
|
|
227
|
+
subject.execute
|
|
228
|
+
sleep 1.5
|
|
229
|
+
send_ami_events_for_dtmf 1
|
|
230
|
+
sleep 1.5
|
|
231
|
+
send_ami_events_for_dtmf 2
|
|
232
|
+
reason.should be_a Punchblock::Component::Input::Complete::NoMatch
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
context '-1' do
|
|
237
|
+
let(:command_opts) { { :inter_digit_timeout => -1 } }
|
|
238
|
+
|
|
239
|
+
it "should not start a timer" do
|
|
240
|
+
subject.wrapped_object.expects(:begin_inter_digit_timer).never
|
|
241
|
+
subject.execute
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
context 'unset' do
|
|
246
|
+
let(:command_opts) { { :inter_digit_timeout => nil } }
|
|
247
|
+
|
|
248
|
+
it "should not start a timer" do
|
|
249
|
+
subject.wrapped_object.expects(:begin_inter_digit_timer).never
|
|
250
|
+
subject.execute
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
context 'a negative number other than -1' do
|
|
255
|
+
let(:command_opts) { { :inter_digit_timeout => -1000 } }
|
|
256
|
+
|
|
257
|
+
it "should return an error and not execute any actions" do
|
|
258
|
+
subject.execute
|
|
259
|
+
error = ProtocolError.new 'option error', 'An inter-digit timeout value that is negative (and not -1) is invalid.'
|
|
260
|
+
command.response(0.1).should == error
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
describe 'sensitivity' do
|
|
266
|
+
pending
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
describe 'min-confidence' do
|
|
270
|
+
pending
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
describe 'max-silence' do
|
|
274
|
+
pending
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|