punchblock 2.6.0 → 2.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/punchblock/component/output.rb +12 -4
- data/lib/punchblock/translator/asterisk.rb +3 -4
- data/lib/punchblock/translator/asterisk/agi_command.rb +3 -0
- data/lib/punchblock/translator/asterisk/call.rb +53 -7
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +1 -1
- data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +1 -1
- data/lib/punchblock/translator/asterisk/component/input.rb +1 -1
- data/lib/punchblock/translator/asterisk/component/mrcp_native_prompt.rb +14 -2
- data/lib/punchblock/translator/asterisk/component/mrcp_recog_prompt.rb +39 -0
- data/lib/punchblock/translator/asterisk/component/output.rb +1 -1
- data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +1 -1
- data/lib/punchblock/version.rb +1 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +250 -35
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +55 -2
- data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +24 -5
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +26 -5
- data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +42 -2
- data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +479 -0
- data/spec/punchblock/translator/asterisk/component/output_spec.rb +40 -17
- data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +4 -3
- data/spec/punchblock/translator/asterisk_spec.rb +36 -9
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 818e14c3ff0bfcf90e7e8087256855a16a1298f4
|
|
4
|
+
data.tar.gz: e0702027282797f03c8c02d2550f94de4b99e4e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8865f044c3d41d8c4410f655fca8c2271a49e29b726bfb6de36a8df936a592a061bd9bf73a6c22e35fe2bc7bfeea478a21b5969b54a218aa1e93d1bc6e43a70
|
|
7
|
+
data.tar.gz: 2951eeea8e8ddf297280b5e488a33768e24a09eae4a0712f144a27a4d8de696d7d1969742493d90e203db9745342714590df5cf44178587be7ddbbc1a84126de
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# [develop](https://github.com/adhearsion/punchblock)
|
|
2
2
|
|
|
3
|
+
# [v2.7.0](https://github.com/adhearsion/punchblock/compare/v2.6.0...v2.7.0) - [2015-06-09](https://rubygems.org/gems/punchblock/versions/2.7.0)
|
|
4
|
+
* Feature: Support for Asterisk 13 (AMI v2)
|
|
5
|
+
* Feature: Pass all possible MRCP recognition headers through Asterisk [#244](https://github.com/adhearsion/punchblock/pull/244)
|
|
6
|
+
* Bugfix: Handle an illegal AMI response by Asterisk as a failure with code -1
|
|
7
|
+
* Bugfix: Ensure a useful error is raised when attempting to join to a call which doesn't exist [#529](https://github.com/adhearsion/adhearsion/issues/529)
|
|
8
|
+
* Bugfix: Support SSML in MRCPRecog case on Asterisk. This is the case where a prompt component wants to render using Asterisk and recognise using UniMRCP. This worked fine with a uri-list which already has test coverage, but threw (and silently swallowed) an exception for an SSML doc (as provided by Adhearsion) resulting in a hung call with no audio, and then a timeout exception in Adhearsion. [#241](https://github.com/adhearsion/punchblock/pull/241)
|
|
9
|
+
|
|
3
10
|
# [v2.6.0](https://github.com/adhearsion/punchblock/compare/v2.5.3...v2.6.0) - [2015-02-01](https://rubygems.org/gems/punchblock/versions/2.6.0)
|
|
4
11
|
* Feature: Support recognition-timeout settings on UniMRCP-based ASR on Asterisk ([#228](https://github.com/adhearsion/punchblock/pull/228))
|
|
5
12
|
* Feature: Implement redirect command on Asterisk ([#219](https://github.com/adhearsion/punchblock/pull/219))
|
|
@@ -46,6 +46,18 @@ module Punchblock
|
|
|
46
46
|
super
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def size
|
|
50
|
+
if ssml?
|
|
51
|
+
value.children.count
|
|
52
|
+
else
|
|
53
|
+
value.size
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ssml?
|
|
58
|
+
content_type == SSML_CONTENT_TYPE
|
|
59
|
+
end
|
|
60
|
+
|
|
49
61
|
private
|
|
50
62
|
|
|
51
63
|
def xml_value
|
|
@@ -58,10 +70,6 @@ module Punchblock
|
|
|
58
70
|
end
|
|
59
71
|
end
|
|
60
72
|
|
|
61
|
-
def ssml?
|
|
62
|
-
content_type == SSML_CONTENT_TYPE
|
|
63
|
-
end
|
|
64
|
-
|
|
65
73
|
def urilist?
|
|
66
74
|
content_type == 'text/uri-list'
|
|
67
75
|
end
|
|
@@ -18,7 +18,7 @@ module Punchblock
|
|
|
18
18
|
autoload :Channel
|
|
19
19
|
autoload :Component
|
|
20
20
|
|
|
21
|
-
attr_reader :ami_client, :connection, :calls
|
|
21
|
+
attr_reader :ami_client, :connection, :calls, :bridges
|
|
22
22
|
|
|
23
23
|
REDIRECT_CONTEXT = 'adhearsion-redirect'
|
|
24
24
|
REDIRECT_EXTENSION = '1'
|
|
@@ -47,7 +47,7 @@ module Punchblock
|
|
|
47
47
|
|
|
48
48
|
def initialize(ami_client, connection)
|
|
49
49
|
@ami_client, @connection = ami_client, connection
|
|
50
|
-
@calls, @components, @channel_to_call_id = {}, {}, {}
|
|
50
|
+
@calls, @components, @channel_to_call_id, @bridges = {}, {}, {}, {}
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def register_call(call)
|
|
@@ -88,7 +88,6 @@ module Punchblock
|
|
|
88
88
|
handle_varset_ami_event event
|
|
89
89
|
|
|
90
90
|
ami_dispatch_to_or_create_call event
|
|
91
|
-
|
|
92
91
|
if !ami_event_known_call?(event) && self.class.event_passes_filter?(event)
|
|
93
92
|
handle_pb_event Event::Asterisk::AMI::Event.new(name: event.name, headers: event.headers)
|
|
94
93
|
end
|
|
@@ -208,7 +207,7 @@ module Punchblock
|
|
|
208
207
|
next if channel.bridged? && !EVENTS_ALLOWED_BRIDGED.include?(event.name)
|
|
209
208
|
call.process_ami_event event
|
|
210
209
|
end
|
|
211
|
-
elsif event.name == "AsyncAGI" && event['SubEvent'] == "Start"
|
|
210
|
+
elsif event.name == "AsyncAGIStart" || (event.name == "AsyncAGI" && event['SubEvent'] == "Start")
|
|
212
211
|
handle_async_agi_start_event event
|
|
213
212
|
end
|
|
214
213
|
end
|
|
@@ -23,6 +23,9 @@ module Punchblock
|
|
|
23
23
|
def parse_result(event)
|
|
24
24
|
parser = RubyAMI::AGIResultParser.new event['Result']
|
|
25
25
|
{code: parser.code, result: parser.result, data: parser.data}
|
|
26
|
+
rescue ArgumentError => e
|
|
27
|
+
pb_logger.warn "Illegal message received from Asterisk: #{e.message}"
|
|
28
|
+
{code: -1, result: nil, data: nil}
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
private
|
|
@@ -13,7 +13,7 @@ module Punchblock
|
|
|
13
13
|
|
|
14
14
|
OUTBOUND_CHANNEL_MATCH = /.* <(?<channel>.*)>/.freeze
|
|
15
15
|
|
|
16
|
-
attr_reader :id, :channel, :translator, :agi_env, :direction
|
|
16
|
+
attr_reader :id, :channel, :translator, :agi_env, :direction, :pending_joins
|
|
17
17
|
|
|
18
18
|
HANGUP_CAUSE_TO_END_REASON = Hash.new { :error }
|
|
19
19
|
HANGUP_CAUSE_TO_END_REASON[0] = :hungup
|
|
@@ -116,19 +116,55 @@ module Punchblock
|
|
|
116
116
|
when 'Hangup'
|
|
117
117
|
handle_hangup_event ami_event['Cause'].to_i, ami_event.best_time
|
|
118
118
|
when 'AsyncAGI'
|
|
119
|
-
|
|
120
|
-
component.handle_ami_event ami_event
|
|
121
|
-
end
|
|
119
|
+
component_for_command_id_handle ami_event
|
|
122
120
|
|
|
123
121
|
if @answered == false && ami_event['SubEvent'] == 'Start'
|
|
124
122
|
@answered = true
|
|
125
123
|
send_pb_event Event::Answered.new(timestamp: ami_event.best_time)
|
|
126
124
|
end
|
|
125
|
+
when 'AsyncAGIStart'
|
|
126
|
+
component_for_command_id_handle ami_event
|
|
127
|
+
|
|
128
|
+
if @answered == false
|
|
129
|
+
@answered = true
|
|
130
|
+
send_pb_event Event::Answered.new(timestamp: ami_event.best_time)
|
|
131
|
+
end
|
|
132
|
+
when 'AsyncAGIExec', 'AsyncAGIEnd'
|
|
133
|
+
component_for_command_id_handle ami_event
|
|
127
134
|
when 'Newstate'
|
|
128
135
|
case ami_event['ChannelState']
|
|
129
136
|
when '5'
|
|
130
137
|
send_pb_event Event::Ringing.new(timestamp: ami_event.best_time)
|
|
131
138
|
end
|
|
139
|
+
when 'BridgeEnter'
|
|
140
|
+
if other_call_channel = translator.bridges.delete(ami_event['BridgeUniqueid'])
|
|
141
|
+
if other_call = translator.call_for_channel(other_call_channel)
|
|
142
|
+
join_command = other_call.pending_joins.delete channel
|
|
143
|
+
join_command.response = true if join_command
|
|
144
|
+
|
|
145
|
+
event = Event::Joined.new call_uri: other_call.id, timestamp: ami_event.best_time
|
|
146
|
+
send_pb_event event
|
|
147
|
+
|
|
148
|
+
other_call_event = Event::Joined.new call_uri: id, timestamp: ami_event.best_time
|
|
149
|
+
other_call_event.target_call_id = other_call.id
|
|
150
|
+
translator.handle_pb_event other_call_event
|
|
151
|
+
end
|
|
152
|
+
else
|
|
153
|
+
translator.bridges[ami_event['BridgeUniqueid']] = ami_event['Channel']
|
|
154
|
+
end
|
|
155
|
+
when 'BridgeLeave'
|
|
156
|
+
if other_call_channel = translator.bridges.delete(ami_event['BridgeUniqueid'] + '_leave')
|
|
157
|
+
if other_call = translator.call_for_channel(other_call_channel)
|
|
158
|
+
event = Event::Unjoined.new call_uri: other_call.id, timestamp: ami_event.best_time
|
|
159
|
+
send_pb_event event
|
|
160
|
+
|
|
161
|
+
other_call_event = Event::Unjoined.new call_uri: id, timestamp: ami_event.best_time
|
|
162
|
+
other_call_event.target_call_id = other_call.id
|
|
163
|
+
translator.handle_pb_event other_call_event
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
translator.bridges[ami_event['BridgeUniqueid'] + '_leave'] = ami_event['Channel']
|
|
167
|
+
end
|
|
132
168
|
when 'OriginateResponse'
|
|
133
169
|
if ami_event['Response'] == 'Failure' && ami_event['Uniqueid'] == '<null>'
|
|
134
170
|
send_end_event :error, nil, ami_event.best_time
|
|
@@ -194,8 +230,12 @@ module Punchblock
|
|
|
194
230
|
command.response = true
|
|
195
231
|
when Command::Join
|
|
196
232
|
other_call = translator.call_with_id command.call_uri
|
|
197
|
-
|
|
198
|
-
|
|
233
|
+
if other_call
|
|
234
|
+
@pending_joins[other_call.channel] = command
|
|
235
|
+
execute_agi_command 'EXEC Bridge', "#{other_call.channel},F(#{REDIRECT_CONTEXT},#{REDIRECT_EXTENSION},#{REDIRECT_PRIORITY})"
|
|
236
|
+
else
|
|
237
|
+
command.response = ProtocolError.new.setup :service_unavailable, "Could not find join party with address #{command.call_uri}", id
|
|
238
|
+
end
|
|
199
239
|
when Command::Unjoin
|
|
200
240
|
other_call = translator.call_with_id command.call_uri
|
|
201
241
|
redirect_back other_call
|
|
@@ -264,7 +304,7 @@ module Punchblock
|
|
|
264
304
|
def execute_agi_command(command, *params)
|
|
265
305
|
agi = AGICommand.new Punchblock.new_uuid, channel, command, *params
|
|
266
306
|
response = Celluloid::Future.new
|
|
267
|
-
register_tmp_handler :ami, name: 'AsyncAGI', [:[], 'SubEvent'] => 'Exec', [:[], 'CommandID'] => agi.id do |event|
|
|
307
|
+
register_tmp_handler :ami, [{name: 'AsyncAGI', [:[], 'SubEvent'] => 'Exec'}, {name: 'AsyncAGIExec'}], [{[:[], 'CommandID'] => agi.id}, {[:[], 'CommandId'] => agi.id}] do |event|
|
|
268
308
|
response.signal Celluloid::SuccessResponse.new(nil, event)
|
|
269
309
|
end
|
|
270
310
|
agi.execute @ami_client
|
|
@@ -353,6 +393,12 @@ module Punchblock
|
|
|
353
393
|
end
|
|
354
394
|
end
|
|
355
395
|
|
|
396
|
+
def component_for_command_id_handle(ami_event)
|
|
397
|
+
if component = component_with_id(ami_event['CommandID'] || ami_event['CommandId'])
|
|
398
|
+
component.handle_ami_event ami_event
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
356
402
|
def variable_for_headers(headers)
|
|
357
403
|
variables = { :punchblock_call_id => id }
|
|
358
404
|
header_counter = 51
|
|
@@ -20,7 +20,7 @@ module Punchblock
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def handle_ami_event(event)
|
|
23
|
-
if event.name == 'AsyncAGI' && event['SubEvent'] == 'Exec'
|
|
23
|
+
if (event.name == 'AsyncAGI' && event['SubEvent'] == 'Exec') || event.name == 'AsyncAGIExec'
|
|
24
24
|
send_complete_event success_reason(event)
|
|
25
25
|
if @component_node.name == 'ASYNCAGI BREAK' && @call.channel_var('PUNCHBLOCK_END_ON_ASYNCAGI_BREAK')
|
|
26
26
|
@call.handle_hangup_event nil, event.best_time
|
|
@@ -58,7 +58,7 @@ module Punchblock
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def register_dtmf_event_handler
|
|
61
|
-
@dtmf_handler_id = call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
|
|
61
|
+
@dtmf_handler_id = call.register_handler :ami, [{:name => 'DTMF', [:[], 'End'] => 'Yes'}, {:name => 'DTMFEnd'}] do |event|
|
|
62
62
|
process_dtmf event['Digit']
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -17,7 +17,7 @@ module Punchblock
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def register_dtmf_event_handler
|
|
20
|
-
call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
|
|
20
|
+
call.register_handler :ami, [{:name => 'DTMF', [:[], 'End'] => 'Yes'}, {:name => 'DTMFEnd'}] do |event|
|
|
21
21
|
process_dtmf event['Digit']
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -17,7 +17,7 @@ module Punchblock
|
|
|
17
17
|
raise OptionError, 'A document is required.' unless output_node.render_documents.count > 0
|
|
18
18
|
raise OptionError, 'Only one document is allowed.' if output_node.render_documents.count > 1
|
|
19
19
|
raise OptionError, 'Only inline documents are allowed.' if first_doc.url
|
|
20
|
-
raise OptionError, 'Only one audio file is allowed.' if first_doc.
|
|
20
|
+
raise OptionError, 'Only one audio file is allowed.' if first_doc.size > 1
|
|
21
21
|
|
|
22
22
|
raise OptionError, 'A grammar is required.' unless input_node.grammars.count > 0
|
|
23
23
|
|
|
@@ -41,7 +41,19 @@ module Punchblock
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def audio_filename
|
|
44
|
-
first_doc.
|
|
44
|
+
path = if first_doc.ssml?
|
|
45
|
+
first_doc.value.children.first.src
|
|
46
|
+
else
|
|
47
|
+
first_doc.value.first
|
|
48
|
+
end.sub('file://', '')
|
|
49
|
+
|
|
50
|
+
dir = File.dirname(path)
|
|
51
|
+
basename = File.basename(path, '.*')
|
|
52
|
+
if dir == '.'
|
|
53
|
+
basename
|
|
54
|
+
else
|
|
55
|
+
File.join(dir, basename)
|
|
56
|
+
end
|
|
45
57
|
end
|
|
46
58
|
|
|
47
59
|
def unimrcp_app_options
|
|
@@ -45,6 +45,13 @@ module Punchblock
|
|
|
45
45
|
raise OptionError, "An initial-timeout value must be -1 or a positive integer." if @initial_timeout < -1
|
|
46
46
|
raise OptionError, "An inter-digit-timeout value must be -1 or a positive integer." if @inter_digit_timeout < -1
|
|
47
47
|
raise OptionError, "A recognition-timeout value must be -1, 0, or a positive integer." if @recognition_timeout < -1
|
|
48
|
+
raise OptionError, "A max-silence value must be -1, 0, or a positive integer." if @max_silence < -1
|
|
49
|
+
raise OptionError, "A speech-complete-timeout value must be -1, 0, or a positive integer." if @speech_complete_timeout < -1
|
|
50
|
+
raise OptionError, "A hotword-max-duration value must be -1, 0, or a positive integer." if @hotword_max_duration < -1
|
|
51
|
+
raise OptionError, "A hotword-min-duration value must be -1, 0, or a positive integer." if @hotword_min_duration < -1
|
|
52
|
+
raise OptionError, "A dtmf-terminate-timeout value must be -1, 0, or a positive integer." if @dtmf_terminate_timeout < -1
|
|
53
|
+
raise OptionError, "An n-best-list-length value must be a positive integer." if @n_best_list_length && @n_best_list_length < 1
|
|
54
|
+
raise OptionError, "A speed-vs-accuracy value must be a positive integer." if @speed_vs_accuracy && @speed_vs_accuracy < 0
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
def execute_app(app, *args)
|
|
@@ -60,6 +67,23 @@ module Punchblock
|
|
|
60
67
|
opts[:ct] = input_node.min_confidence if input_node.min_confidence
|
|
61
68
|
opts[:sl] = input_node.sensitivity if input_node.sensitivity
|
|
62
69
|
opts[:t] = input_node.recognition_timeout if @recognition_timeout > -1
|
|
70
|
+
opts[:sint] = input_node.max_silence if @max_silence > -1
|
|
71
|
+
opts[:sct] = @speech_complete_timeout if @speech_complete_timeout > -1
|
|
72
|
+
|
|
73
|
+
opts[:sva] = @speed_vs_accuracy if @speed_vs_accuracy
|
|
74
|
+
opts[:nb] = @n_best_list_length if @n_best_list_length
|
|
75
|
+
opts[:sit] = @start_input_timers unless @start_input_timers.nil?
|
|
76
|
+
opts[:dtt] = @dtmf_terminate_timeout if @dtmf_terminate_timeout > -1
|
|
77
|
+
opts[:sw] = @save_waveform unless @save_waveform.nil?
|
|
78
|
+
opts[:nac] = @new_audio_channel unless @new_audio_channel.nil?
|
|
79
|
+
opts[:rm] = @recognition_mode if @recognition_mode
|
|
80
|
+
opts[:hmaxd] = @hotword_max_duration if @hotword_max_duration > -1
|
|
81
|
+
opts[:hmind] = @hotword_min_duration if @hotword_min_duration > -1
|
|
82
|
+
opts[:cdb] = @clear_dtmf_buffer unless @clear_dtmf_buffer.nil?
|
|
83
|
+
opts[:enm] = @early_no_match unless @early_no_match.nil?
|
|
84
|
+
opts[:iwu] = @input_waveform_uri if @input_waveform_uri
|
|
85
|
+
opts[:mt] = @media_type if @media_type
|
|
86
|
+
|
|
63
87
|
yield opts
|
|
64
88
|
end
|
|
65
89
|
end
|
|
@@ -68,6 +92,21 @@ module Punchblock
|
|
|
68
92
|
@initial_timeout = input_node.initial_timeout || -1
|
|
69
93
|
@inter_digit_timeout = input_node.inter_digit_timeout || -1
|
|
70
94
|
@recognition_timeout = input_node.recognition_timeout || -1
|
|
95
|
+
@max_silence = input_node.max_silence || -1
|
|
96
|
+
@speech_complete_timeout = input_node.headers['Speech-Complete-Timeout'] || -1
|
|
97
|
+
@speed_vs_accuracy = input_node.headers['Speed-Vs-Accuracy']
|
|
98
|
+
@n_best_list_length = input_node.headers['N-Best-List-Length']
|
|
99
|
+
@start_input_timers = input_node.headers['Start-Input-Timers']
|
|
100
|
+
@dtmf_terminate_timeout = input_node.headers['DTMF-Terminate-Timeout'] || -1
|
|
101
|
+
@save_waveform = input_node.headers['Save-Waveform']
|
|
102
|
+
@new_audio_channel = input_node.headers['New-Audio-Channel']
|
|
103
|
+
@recognition_mode = input_node.headers['Recognition-Mode']
|
|
104
|
+
@hotword_max_duration = input_node.headers['Hotword-Max-Duration'] || -1
|
|
105
|
+
@hotword_min_duration = input_node.headers['Hotword-Min-Duration'] || -1
|
|
106
|
+
@clear_dtmf_buffer = input_node.headers['Clear-DTMF-Buffer']
|
|
107
|
+
@early_no_match = input_node.headers['Early-No-Match']
|
|
108
|
+
@input_waveform_uri = input_node.headers['Input-Waveform-URI']
|
|
109
|
+
@media_type = input_node.headers['Media-Type']
|
|
71
110
|
end
|
|
72
111
|
|
|
73
112
|
def grammars
|
|
@@ -107,7 +107,7 @@ module Punchblock
|
|
|
107
107
|
send_progress_if_necessary
|
|
108
108
|
|
|
109
109
|
if interrupt
|
|
110
|
-
call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
|
|
110
|
+
call.register_handler :ami, [{:name => 'DTMF', [:[], 'End'] => 'Yes'}, {:name => 'DTMFEnd'}] do |event|
|
|
111
111
|
stop_by_redirect finish_reason
|
|
112
112
|
end
|
|
113
113
|
end
|
|
@@ -18,7 +18,7 @@ module Punchblock
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def stop_by_redirect(complete_reason)
|
|
21
|
-
call.register_handler :ami,
|
|
21
|
+
call.register_handler :ami, [{name: 'AsyncAGI', [:[], 'SubEvent'] => 'Start'}, {name: 'AsyncAGIExec'}] do |event|
|
|
22
22
|
send_complete_event complete_reason
|
|
23
23
|
end
|
|
24
24
|
call.redirect_back
|
data/lib/punchblock/version.rb
CHANGED
|
@@ -516,31 +516,80 @@ module Punchblock
|
|
|
516
516
|
let :component do
|
|
517
517
|
Component::Asterisk::AGICommand.new mock_component_node, subject
|
|
518
518
|
end
|
|
519
|
-
|
|
520
|
-
let(:ami_event) do
|
|
521
|
-
RubyAMI::Event.new "AsyncAGI",
|
|
522
|
-
"SubEvent" => "End",
|
|
523
|
-
"Channel" => "SIP/1234-00000000",
|
|
524
|
-
"CommandID" => component.id,
|
|
525
|
-
"Command" => "EXEC ANSWER",
|
|
526
|
-
"Result" => "200%20result=123%20(timeout)%0A"
|
|
527
|
-
end
|
|
528
|
-
|
|
529
519
|
before do
|
|
530
520
|
subject.register_component component
|
|
531
521
|
end
|
|
532
522
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
523
|
+
context 'with Asterisk 11 AsyncAGI SubEvent' do
|
|
524
|
+
let(:ami_event) do
|
|
525
|
+
RubyAMI::Event.new "AsyncAGI",
|
|
526
|
+
"SubEvent" => "End",
|
|
527
|
+
"Channel" => "SIP/1234-00000000",
|
|
528
|
+
"CommandID" => component.id,
|
|
529
|
+
"Command" => "EXEC ANSWER",
|
|
530
|
+
"Result" => "200%20result=123%20(timeout)%0A"
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
it 'should send the event to the component' do
|
|
534
|
+
expect(component).to receive(:handle_ami_event).once.with ami_event
|
|
535
|
+
subject.process_ami_event ami_event
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
it 'should not send an answered event' do
|
|
539
|
+
expect(translator).to receive(:handle_pb_event).with(kind_of(Punchblock::Event::Answered)).never
|
|
540
|
+
subject.process_ami_event ami_event
|
|
541
|
+
end
|
|
536
542
|
end
|
|
537
543
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
544
|
+
context 'with Asterisk 13 AsyncAGIEnd and CommandId with a lowercase d' do
|
|
545
|
+
let(:ami_event) do
|
|
546
|
+
RubyAMI::Event.new "AsyncAGIEnd",
|
|
547
|
+
"Channel" => "SIP/1234-00000000",
|
|
548
|
+
"CommandId" => component.id,
|
|
549
|
+
"Command" => "EXEC ANSWER",
|
|
550
|
+
"Result" => "200%20result=123%20(timeout)%0A"
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
it 'should send the event to the component' do
|
|
554
|
+
expect(component).to receive(:handle_ami_event).once.with ami_event
|
|
555
|
+
subject.process_ami_event ami_event
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
it 'should not send an answered event' do
|
|
559
|
+
expect(translator).to receive(:handle_pb_event).with(kind_of(Punchblock::Event::Answered)).never
|
|
560
|
+
subject.process_ami_event ami_event
|
|
561
|
+
end
|
|
541
562
|
end
|
|
542
563
|
end
|
|
543
564
|
|
|
565
|
+
def should_send_answered_event
|
|
566
|
+
expected_answered = Punchblock::Event::Answered.new
|
|
567
|
+
expected_answered.target_call_id = subject.id
|
|
568
|
+
expect(translator).to receive(:handle_pb_event).with expected_answered
|
|
569
|
+
subject.process_ami_event ami_event
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def answered_should_be_true
|
|
573
|
+
subject.process_ami_event ami_event
|
|
574
|
+
expect(subject.answered?).to be_true
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def should_only_send_one_answered_event
|
|
578
|
+
expected_answered = Punchblock::Event::Answered.new
|
|
579
|
+
expected_answered.target_call_id = subject.id
|
|
580
|
+
expect(translator).to receive(:handle_pb_event).with(expected_answered).once
|
|
581
|
+
subject.process_ami_event ami_event
|
|
582
|
+
subject.process_ami_event ami_event
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def should_use_ami_timestamp_for_rayo_event
|
|
586
|
+
expected_answered = Punchblock::Event::Answered.new target_call_id: subject.id,
|
|
587
|
+
timestamp: DateTime.new(2014, 2, 25, 22, 46, 20)
|
|
588
|
+
expect(translator).to receive(:handle_pb_event).with expected_answered
|
|
589
|
+
|
|
590
|
+
subject.process_ami_event ami_event
|
|
591
|
+
end
|
|
592
|
+
|
|
544
593
|
context 'with an AsyncAGI Start event' do
|
|
545
594
|
let(:ami_event) do
|
|
546
595
|
RubyAMI::Event.new "AsyncAGI",
|
|
@@ -550,24 +599,16 @@ module Punchblock
|
|
|
550
599
|
end
|
|
551
600
|
|
|
552
601
|
it 'should send an answered event' do
|
|
553
|
-
|
|
554
|
-
expected_answered.target_call_id = subject.id
|
|
555
|
-
expect(translator).to receive(:handle_pb_event).with expected_answered
|
|
556
|
-
subject.process_ami_event ami_event
|
|
602
|
+
should_send_answered_event
|
|
557
603
|
end
|
|
558
604
|
|
|
559
605
|
it '#answered? should be true' do
|
|
560
|
-
|
|
561
|
-
expect(subject.answered?).to be_true
|
|
606
|
+
answered_should_be_true
|
|
562
607
|
end
|
|
563
608
|
|
|
564
609
|
context "for a second time" do
|
|
565
610
|
it 'should only send one answered event' do
|
|
566
|
-
|
|
567
|
-
expected_answered.target_call_id = subject.id
|
|
568
|
-
expect(translator).to receive(:handle_pb_event).with(expected_answered).once
|
|
569
|
-
subject.process_ami_event ami_event
|
|
570
|
-
subject.process_ami_event ami_event
|
|
611
|
+
should_only_send_one_answered_event
|
|
571
612
|
end
|
|
572
613
|
end
|
|
573
614
|
|
|
@@ -581,11 +622,42 @@ module Punchblock
|
|
|
581
622
|
end
|
|
582
623
|
|
|
583
624
|
it "should use the AMI timestamp for the Rayo event" do
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
625
|
+
should_use_ami_timestamp_for_rayo_event
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
end
|
|
587
629
|
|
|
588
|
-
|
|
630
|
+
context 'with an Asterisk 13 AsyncAGIStart event' do
|
|
631
|
+
let(:ami_event) do
|
|
632
|
+
RubyAMI::Event.new "AsyncAGIStart",
|
|
633
|
+
"Channel" => "SIP/1234-00000000",
|
|
634
|
+
"Env" => "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2Fuserb-00000006%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201390303636.6%0Aagi_version%3A%2011.7.0%0Aagi_callerid%3A%20userb%0Aagi_calleridname%3A%20User%20B%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%20unknown%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20adhearsion-redirect%0Aagi_extension%3A%201%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%20139696536876800%0A%0A"
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
it 'should send an answered event' do
|
|
638
|
+
should_send_answered_event
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
it '#answered? should be true' do
|
|
642
|
+
answered_should_be_true
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
context "for a second time" do
|
|
646
|
+
it 'should only send one answered event' do
|
|
647
|
+
should_only_send_one_answered_event
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
context "when the AMI event has a timestamp" do
|
|
652
|
+
let :ami_event do
|
|
653
|
+
RubyAMI::Event.new "AsyncAGIStart",
|
|
654
|
+
"Channel" => "SIP/1234-00000000",
|
|
655
|
+
"Env" => "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2Fuserb-00000006%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201390303636.6%0Aagi_version%3A%2011.7.0%0Aagi_callerid%3A%20userb%0Aagi_calleridname%3A%20User%20B%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%20unknown%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20adhearsion-redirect%0Aagi_extension%3A%201%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%20139696536876800%0A%0A",
|
|
656
|
+
'Timestamp' => '1393368380.572575'
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
it "should use the AMI timestamp for the Rayo event" do
|
|
660
|
+
should_use_ami_timestamp_for_rayo_event
|
|
589
661
|
end
|
|
590
662
|
end
|
|
591
663
|
end
|
|
@@ -736,6 +808,124 @@ module Punchblock
|
|
|
736
808
|
end
|
|
737
809
|
end
|
|
738
810
|
|
|
811
|
+
context 'with a BridgeEnter event' do
|
|
812
|
+
let(:bridge_uniqueid) { "1234-5678" }
|
|
813
|
+
let(:call_channel) { "SIP/foo" }
|
|
814
|
+
let :ami_event do
|
|
815
|
+
RubyAMI::Event.new 'BridgeEnter',
|
|
816
|
+
'Privilege' => "call,all",
|
|
817
|
+
'BridgeUniqueid' => bridge_uniqueid,
|
|
818
|
+
'Channel' => call_channel
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
context 'when the event is received the first time' do
|
|
822
|
+
it 'sets an entry in translator.bridges' do
|
|
823
|
+
subject.process_ami_event ami_event
|
|
824
|
+
expect(translator.bridges[bridge_uniqueid]).to eq call_channel
|
|
825
|
+
end
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
context 'when the event is received a second time for the same BridgeUniqueid' do
|
|
829
|
+
let(:other_channel) { 'SIP/5678-00000000' }
|
|
830
|
+
let :other_call do
|
|
831
|
+
Call.new other_channel, translator, ami_client, connection
|
|
832
|
+
end
|
|
833
|
+
let(:other_call_id) { other_call.id }
|
|
834
|
+
|
|
835
|
+
let :command do
|
|
836
|
+
Punchblock::Command::Join.new call_uri: other_call_id
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
let :ami_event do
|
|
840
|
+
RubyAMI::Event.new 'BridgeEnter',
|
|
841
|
+
'Privilege' => "call,all",
|
|
842
|
+
'BridgeUniqueid' => bridge_uniqueid,
|
|
843
|
+
'Channel' => call_channel
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
let :expected_joined do
|
|
847
|
+
Punchblock::Event::Joined.new target_call_id: subject.id,
|
|
848
|
+
call_uri: other_call_id
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
let :expected_joined_other do
|
|
852
|
+
Punchblock::Event::Joined.new target_call_id: other_call_id,
|
|
853
|
+
call_uri: subject.id
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
before do
|
|
857
|
+
translator.bridges[bridge_uniqueid] = other_channel
|
|
858
|
+
translator.register_call other_call
|
|
859
|
+
|
|
860
|
+
other_call.pending_joins[channel] = command
|
|
861
|
+
command.request!
|
|
862
|
+
expect(subject).to receive(:execute_agi_command).and_return code: 200
|
|
863
|
+
subject.execute_command command
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
it 'sends the correct Joined events' do
|
|
867
|
+
expect(translator).to receive(:handle_pb_event).with expected_joined
|
|
868
|
+
expect(translator).to receive(:handle_pb_event).with expected_joined_other
|
|
869
|
+
subject.process_ami_event ami_event
|
|
870
|
+
expect(command.response(0.5)).to eq(true)
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
context 'with a BridgeLeave event' do
|
|
876
|
+
let(:bridge_uniqueid) { "1234-5678" }
|
|
877
|
+
let(:call_channel) { "SIP/foo-1234" }
|
|
878
|
+
let :ami_event do
|
|
879
|
+
RubyAMI::Event.new 'BridgeLeave',
|
|
880
|
+
'Privilege' => "call,all",
|
|
881
|
+
'BridgeUniqueid' => bridge_uniqueid,
|
|
882
|
+
'Channel' => call_channel
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
context 'when the event is received the first time' do
|
|
886
|
+
it 'sets an entry in translator.bridges' do
|
|
887
|
+
subject.process_ami_event ami_event
|
|
888
|
+
expect(translator.bridges[bridge_uniqueid + '_leave']).to eq call_channel
|
|
889
|
+
end
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
context 'when the event is received a second time for the same BridgeUniqueid' do
|
|
893
|
+
let(:other_channel) { 'SIP/5678-00000000' }
|
|
894
|
+
let :other_call do
|
|
895
|
+
Call.new other_channel, translator, ami_client, connection
|
|
896
|
+
end
|
|
897
|
+
let(:other_call_id) { other_call.id }
|
|
898
|
+
|
|
899
|
+
let :ami_event do
|
|
900
|
+
RubyAMI::Event.new 'BridgeLeave',
|
|
901
|
+
'Privilege' => "call,all",
|
|
902
|
+
'BridgeUniqueid' => bridge_uniqueid,
|
|
903
|
+
'Channel' => call_channel
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
let :expected_unjoined do
|
|
907
|
+
Punchblock::Event::Unjoined.new target_call_id: subject.id,
|
|
908
|
+
call_uri: other_call_id
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
let :expected_unjoined_other do
|
|
912
|
+
Punchblock::Event::Unjoined.new target_call_id: other_call_id,
|
|
913
|
+
call_uri: subject.id
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
before do
|
|
917
|
+
translator.bridges[bridge_uniqueid + '_leave'] = other_channel
|
|
918
|
+
translator.register_call other_call
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
it 'sends the correct Unjoined events' do
|
|
922
|
+
expect(translator).to receive(:handle_pb_event).with expected_unjoined
|
|
923
|
+
expect(translator).to receive(:handle_pb_event).with expected_unjoined_other
|
|
924
|
+
subject.process_ami_event ami_event
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
end
|
|
928
|
+
|
|
739
929
|
context 'with a BridgeExec event' do
|
|
740
930
|
let :ami_event do
|
|
741
931
|
RubyAMI::Event.new 'BridgeExec',
|
|
@@ -794,10 +984,10 @@ module Punchblock
|
|
|
794
984
|
|
|
795
985
|
context 'with a Bridge event' do
|
|
796
986
|
let(:other_channel) { 'SIP/5678-00000000' }
|
|
797
|
-
let(:other_call_id) { 'def567' }
|
|
798
987
|
let :other_call do
|
|
799
988
|
Call.new other_channel, translator, ami_client, connection
|
|
800
989
|
end
|
|
990
|
+
let(:other_call_id) { other_call.id }
|
|
801
991
|
|
|
802
992
|
let :ami_event do
|
|
803
993
|
RubyAMI::Event.new 'Bridge',
|
|
@@ -828,7 +1018,6 @@ module Punchblock
|
|
|
828
1018
|
before do
|
|
829
1019
|
translator.register_call other_call
|
|
830
1020
|
expect(translator).to receive(:call_for_channel).with(other_channel).and_return(other_call)
|
|
831
|
-
expect(other_call).to receive(:id).and_return other_call_id
|
|
832
1021
|
end
|
|
833
1022
|
|
|
834
1023
|
context "of state 'Link'" do
|
|
@@ -1379,6 +1568,15 @@ module Punchblock
|
|
|
1379
1568
|
subject.execute_command command
|
|
1380
1569
|
end
|
|
1381
1570
|
|
|
1571
|
+
context "when the other call doesn't exist" do
|
|
1572
|
+
let(:other_call) { nil }
|
|
1573
|
+
|
|
1574
|
+
it "returns an error" do
|
|
1575
|
+
subject.execute_command command
|
|
1576
|
+
expect(command.response(0.5)).to eq(ProtocolError.new.setup(:service_unavailable, "Could not find join party with address #{other_call_id}", subject.id))
|
|
1577
|
+
end
|
|
1578
|
+
end
|
|
1579
|
+
|
|
1382
1580
|
context "when the AMI command raises an error" do
|
|
1383
1581
|
let(:message) { 'Some error' }
|
|
1384
1582
|
let(:error) { RubyAMI::Error.new.tap { |e| e.message = message } }
|
|
@@ -1758,6 +1956,7 @@ module Punchblock
|
|
|
1758
1956
|
end
|
|
1759
1957
|
end
|
|
1760
1958
|
|
|
1959
|
+
|
|
1761
1960
|
describe 'when receiving an AsyncAGI event' do
|
|
1762
1961
|
context 'of type Exec' do
|
|
1763
1962
|
let(:ami_event) do
|
|
@@ -1771,11 +1970,27 @@ module Punchblock
|
|
|
1771
1970
|
|
|
1772
1971
|
it 'should return the result' do
|
|
1773
1972
|
fut = Celluloid::Future.new { subject.execute_agi_command 'EXEC ANSWER' }
|
|
1774
|
-
|
|
1775
1973
|
sleep 0.25
|
|
1776
|
-
|
|
1777
1974
|
subject.process_ami_event ami_event
|
|
1975
|
+
expect(fut.value).to eq({code: 200, result: 123, data: 'timeout'})
|
|
1976
|
+
end
|
|
1977
|
+
end
|
|
1978
|
+
end
|
|
1778
1979
|
|
|
1980
|
+
describe 'when receiving an Asterisk 13 AsyncAGIExec event' do
|
|
1981
|
+
context 'without a subtype' do
|
|
1982
|
+
let(:ami_event) do
|
|
1983
|
+
RubyAMI::Event.new 'AsyncAGIExec',
|
|
1984
|
+
"Channel" => channel,
|
|
1985
|
+
"CommandId" => Punchblock.new_uuid,
|
|
1986
|
+
"Command" => "EXEC ANSWER",
|
|
1987
|
+
"Result" => "200%20result=123%20(timeout)%0A"
|
|
1988
|
+
end
|
|
1989
|
+
|
|
1990
|
+
it 'should return the result' do
|
|
1991
|
+
fut = Celluloid::Future.new { subject.execute_agi_command 'EXEC ANSWER' }
|
|
1992
|
+
sleep 0.25
|
|
1993
|
+
subject.process_ami_event ami_event
|
|
1779
1994
|
expect(fut.value).to eq({code: 200, result: 123, data: 'timeout'})
|
|
1780
1995
|
end
|
|
1781
1996
|
end
|