punchblock 2.6.0 → 2.7.0

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