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.
Files changed (24) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/lib/punchblock/component/output.rb +12 -4
  4. data/lib/punchblock/translator/asterisk.rb +3 -4
  5. data/lib/punchblock/translator/asterisk/agi_command.rb +3 -0
  6. data/lib/punchblock/translator/asterisk/call.rb +53 -7
  7. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +1 -1
  8. data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +1 -1
  9. data/lib/punchblock/translator/asterisk/component/input.rb +1 -1
  10. data/lib/punchblock/translator/asterisk/component/mrcp_native_prompt.rb +14 -2
  11. data/lib/punchblock/translator/asterisk/component/mrcp_recog_prompt.rb +39 -0
  12. data/lib/punchblock/translator/asterisk/component/output.rb +1 -1
  13. data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +1 -1
  14. data/lib/punchblock/version.rb +1 -1
  15. data/spec/punchblock/translator/asterisk/call_spec.rb +250 -35
  16. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +55 -2
  17. data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +24 -5
  18. data/spec/punchblock/translator/asterisk/component/input_spec.rb +26 -5
  19. data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +42 -2
  20. data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +479 -0
  21. data/spec/punchblock/translator/asterisk/component/output_spec.rb +40 -17
  22. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +4 -3
  23. data/spec/punchblock/translator/asterisk_spec.rb +36 -9
  24. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ade03e2779ab1cdbeec2d6f3745571abc1713b81
4
- data.tar.gz: accbf90369fded5cb31bb010b2fbe02c9f887ec3
3
+ metadata.gz: 818e14c3ff0bfcf90e7e8087256855a16a1298f4
4
+ data.tar.gz: e0702027282797f03c8c02d2550f94de4b99e4e8
5
5
  SHA512:
6
- metadata.gz: 4b3c942cfd4e6f6acd1dd8d2042dc0ef7cd50fb795ef2a0e96fcfa97afba700c5778c2c89637a61d79704be3f0d2f0849aea353ce269dd67fb82f293cb4a0eb3
7
- data.tar.gz: 3c0ddd90d0c21bd82de65d3d792fc0d0f898acd6d2e728a2a03482c86dd001665e38e7ce5c2a881a413bb5c0e9771c85c099f33323ae2d68db7033c93733e60d
6
+ metadata.gz: e8865f044c3d41d8c4410f655fca8c2271a49e29b726bfb6de36a8df936a592a061bd9bf73a6c22e35fe2bc7bfeea478a21b5969b54a218aa1e93d1bc6e43a70
7
+ data.tar.gz: 2951eeea8e8ddf297280b5e488a33768e24a09eae4a0712f144a27a4d8de696d7d1969742493d90e203db9745342714590df5cf44178587be7ddbbc1a84126de
@@ -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
- if component = component_with_id(ami_event['CommandID'])
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
- @pending_joins[other_call.channel] = command
198
- execute_agi_command 'EXEC Bridge', "#{other_call.channel},F(#{REDIRECT_CONTEXT},#{REDIRECT_EXTENSION},#{REDIRECT_PRIORITY})"
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.value.size > 1
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.value.first
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, lambda { |e| e['SubEvent'] == 'Start' }, :name => 'AsyncAGI' do |event|
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
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Punchblock
4
- VERSION = "2.6.0"
4
+ VERSION = "2.7.0"
5
5
  end
@@ -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
- 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
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
- 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
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
- expected_answered = Punchblock::Event::Answered.new
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
- subject.process_ami_event ami_event
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
- 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).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
- expected_answered = Punchblock::Event::Answered.new target_call_id: subject.id,
585
- timestamp: DateTime.new(2014, 2, 25, 22, 46, 20)
586
- expect(translator).to receive(:handle_pb_event).with expected_answered
625
+ should_use_ami_timestamp_for_rayo_event
626
+ end
627
+ end
628
+ end
587
629
 
588
- subject.process_ami_event ami_event
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