punchblock 0.8.4 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|