punchblock 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +3 -3
- data/CHANGELOG.md +23 -0
- data/lib/punchblock.rb +24 -0
- data/lib/punchblock/command/reject.rb +10 -2
- data/lib/punchblock/component/record.rb +16 -0
- data/lib/punchblock/core_ext/blather/stanza.rb +3 -1
- data/lib/punchblock/dead_actor_safety.rb +9 -0
- data/lib/punchblock/event/complete.rb +9 -11
- data/lib/punchblock/rayo_node.rb +4 -0
- data/lib/punchblock/translator/asterisk.rb +65 -22
- data/lib/punchblock/translator/asterisk/call.rb +49 -30
- data/lib/punchblock/translator/asterisk/component.rb +6 -8
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +13 -20
- data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +1 -1
- data/lib/punchblock/translator/asterisk/component/input.rb +3 -6
- data/lib/punchblock/translator/asterisk/component/output.rb +40 -45
- data/lib/punchblock/translator/asterisk/component/record.rb +1 -1
- data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +5 -2
- data/lib/punchblock/version.rb +1 -1
- data/punchblock.gemspec +5 -5
- data/spec/punchblock/command/reject_spec.rb +7 -1
- data/spec/punchblock/command_node_spec.rb +5 -2
- data/spec/punchblock/component/component_node_spec.rb +4 -0
- data/spec/punchblock/component/output_spec.rb +1 -1
- data/spec/punchblock/component/record_spec.rb +30 -0
- data/spec/punchblock/event/complete_spec.rb +10 -0
- data/spec/punchblock/translator/asterisk/call_spec.rb +191 -48
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +6 -39
- data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +3 -3
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +8 -3
- data/spec/punchblock/translator/asterisk/component/output_spec.rb +153 -46
- data/spec/punchblock/translator/asterisk/component/record_spec.rb +6 -5
- data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +1 -2
- data/spec/punchblock/translator/asterisk/component_spec.rb +1 -0
- data/spec/punchblock/translator/asterisk_spec.rb +147 -12
- data/spec/punchblock_spec.rb +34 -0
- data/spec/spec_helper.rb +5 -1
- metadata +30 -20
@@ -16,9 +16,9 @@ module Punchblock
|
|
16
16
|
end
|
17
17
|
let(:translator) { Punchblock::Translator::Asterisk.new mock('AMI'), connection }
|
18
18
|
let(:mock_call) { Punchblock::Translator::Asterisk::Call.new channel, translator }
|
19
|
-
let(:component_id) {
|
19
|
+
let(:component_id) { Punchblock.new_uuid }
|
20
20
|
|
21
|
-
before {
|
21
|
+
before { stub_uuids component_id }
|
22
22
|
|
23
23
|
let :command do
|
24
24
|
Punchblock::Component::Asterisk::AGI::Command.new :name => 'EXEC ANSWER'
|
@@ -31,8 +31,10 @@ module Punchblock
|
|
31
31
|
end
|
32
32
|
|
33
33
|
context 'initial execution' do
|
34
|
+
before { command.request! }
|
35
|
+
|
34
36
|
it 'should send the appropriate RubyAMI::Action' do
|
35
|
-
mock_call.expects(:send_ami_action
|
37
|
+
mock_call.expects(:send_ami_action).once.with(expected_action).returns(expected_action)
|
36
38
|
subject.execute
|
37
39
|
end
|
38
40
|
|
@@ -48,7 +50,7 @@ module Punchblock
|
|
48
50
|
end
|
49
51
|
|
50
52
|
it 'should send the appropriate RubyAMI::Action' do
|
51
|
-
mock_call.expects(:send_ami_action
|
53
|
+
mock_call.expects(:send_ami_action).once.with(expected_action).returns(expected_action)
|
52
54
|
subject.execute
|
53
55
|
end
|
54
56
|
end
|
@@ -125,41 +127,6 @@ module Punchblock
|
|
125
127
|
end
|
126
128
|
end
|
127
129
|
end
|
128
|
-
|
129
|
-
describe '#parse_agi_result' do
|
130
|
-
context 'with a simple result with no data' do
|
131
|
-
let(:result_string) { "200%20result=123%0A" }
|
132
|
-
|
133
|
-
it 'should provide the code and result' do
|
134
|
-
code, result, data = subject.parse_agi_result result_string
|
135
|
-
code.should be == 200
|
136
|
-
result.should be == 123
|
137
|
-
data.should be == ''
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
context 'with a result and data in parens' do
|
142
|
-
let(:result_string) { "200%20result=-123%20(timeout)%0A" }
|
143
|
-
|
144
|
-
it 'should provide the code and result' do
|
145
|
-
code, result, data = subject.parse_agi_result result_string
|
146
|
-
code.should be == 200
|
147
|
-
result.should be == -123
|
148
|
-
data.should be == 'timeout'
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
context 'with a result and key-value data' do
|
153
|
-
let(:result_string) { "200%20result=123%20foo=bar%0A" }
|
154
|
-
|
155
|
-
it 'should provide the code and result' do
|
156
|
-
code, result, data = subject.parse_agi_result result_string
|
157
|
-
code.should be == 200
|
158
|
-
result.should be == 123
|
159
|
-
data.should be == 'foo=bar'
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
130
|
end
|
164
131
|
end
|
165
132
|
end
|
@@ -26,16 +26,16 @@ module Punchblock
|
|
26
26
|
end
|
27
27
|
|
28
28
|
context 'initial execution' do
|
29
|
-
let(:component_id) {
|
29
|
+
let(:component_id) { Punchblock.new_uuid }
|
30
30
|
|
31
31
|
let :expected_response do
|
32
32
|
Ref.new :id => component_id
|
33
33
|
end
|
34
34
|
|
35
|
-
before {
|
35
|
+
before { stub_uuids component_id }
|
36
36
|
|
37
37
|
it 'should send the appropriate RubyAMI::Action and send the component node a ref with the action ID' do
|
38
|
-
mock_translator.expects(:send_ami_action
|
38
|
+
mock_translator.expects(:send_ami_action).once.with(expected_action).returns(expected_action)
|
39
39
|
command.expects(:response=).once.with(expected_response)
|
40
40
|
subject.execute
|
41
41
|
end
|
@@ -42,12 +42,12 @@ module Punchblock
|
|
42
42
|
describe '#execute' do
|
43
43
|
before { original_command.request! }
|
44
44
|
|
45
|
-
it "calls
|
46
|
-
call.expects
|
45
|
+
it "calls send_progress on the call" do
|
46
|
+
call.expects(:send_progress)
|
47
47
|
subject.execute
|
48
48
|
end
|
49
49
|
|
50
|
-
before { call.stubs :
|
50
|
+
before { call.stubs :send_progress }
|
51
51
|
|
52
52
|
let(:original_command_opts) { {} }
|
53
53
|
|
@@ -94,6 +94,11 @@ module Punchblock
|
|
94
94
|
it "should send a success complete event with the relevant data" do
|
95
95
|
reason.should be == expected_event
|
96
96
|
end
|
97
|
+
|
98
|
+
it "should not process further dtmf events" do
|
99
|
+
subject.expects(:process_dtmf!).never
|
100
|
+
send_ami_events_for_dtmf 3
|
101
|
+
end
|
97
102
|
end
|
98
103
|
|
99
104
|
context "when the match is invalid" do
|
@@ -32,16 +32,13 @@ module Punchblock
|
|
32
32
|
|
33
33
|
subject { Output.new original_command, mock_call }
|
34
34
|
|
35
|
+
def expect_answered(value = true)
|
36
|
+
mock_call.expects(:answered?).returns(value).at_least_once
|
37
|
+
end
|
38
|
+
|
35
39
|
describe '#execute' do
|
36
40
|
before { original_command.request! }
|
37
41
|
|
38
|
-
it "calls answer_if_not_answered on the call" do
|
39
|
-
mock_call.expects :answer_if_not_answered
|
40
|
-
subject.execute
|
41
|
-
end
|
42
|
-
|
43
|
-
before { mock_call.stubs :answer_if_not_answered }
|
44
|
-
|
45
42
|
context 'with a media engine of :swift' do
|
46
43
|
let(:media_engine) { :swift }
|
47
44
|
|
@@ -90,6 +87,7 @@ module Punchblock
|
|
90
87
|
context "set to :any" do
|
91
88
|
let(:command_opts) { { :interrupt_on => :any } }
|
92
89
|
it "should add the interrupt options to the argument" do
|
90
|
+
expect_answered
|
93
91
|
mock_call.expects(:send_agi_action!).once.with 'EXEC Swift', ssml_with_options('', '|1|1')
|
94
92
|
subject.execute
|
95
93
|
end
|
@@ -98,6 +96,7 @@ module Punchblock
|
|
98
96
|
context "set to :dtmf" do
|
99
97
|
let(:command_opts) { { :interrupt_on => :dtmf } }
|
100
98
|
it "should add the interrupt options to the argument" do
|
99
|
+
expect_answered
|
101
100
|
mock_call.expects(:send_agi_action!).once.with 'EXEC Swift', ssml_with_options('', '|1|1')
|
102
101
|
subject.execute
|
103
102
|
end
|
@@ -307,6 +306,7 @@ module Punchblock
|
|
307
306
|
context "set to :any" do
|
308
307
|
let(:command_opts) { { :interrupt_on => :any } }
|
309
308
|
it "should pass the i option to MRCPSynth" do
|
309
|
+
expect_answered
|
310
310
|
expect_mrcpsynth_with_options(/i=any/)
|
311
311
|
subject.execute
|
312
312
|
end
|
@@ -315,6 +315,7 @@ module Punchblock
|
|
315
315
|
context "set to :dtmf" do
|
316
316
|
let(:command_opts) { { :interrupt_on => :dtmf } }
|
317
317
|
it "should pass the i option to MRCPSynth" do
|
318
|
+
expect_answered
|
318
319
|
expect_mrcpsynth_with_options(/i=any/)
|
319
320
|
subject.execute
|
320
321
|
end
|
@@ -334,12 +335,12 @@ module Punchblock
|
|
334
335
|
context 'with a media engine of :asterisk' do
|
335
336
|
let(:media_engine) { :asterisk }
|
336
337
|
|
337
|
-
def
|
338
|
-
mock_call.expects(:send_agi_action!).once.with '
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
338
|
+
def expect_playback(filename = audio_filename)
|
339
|
+
mock_call.expects(:send_agi_action!).once.with 'EXEC Playback', filename
|
340
|
+
end
|
341
|
+
|
342
|
+
def expect_playback_noanswer
|
343
|
+
mock_call.expects(:send_agi_action!).once.with 'EXEC Playback', audio_filename + ',noanswer'
|
343
344
|
end
|
344
345
|
|
345
346
|
let(:audio_filename) { 'http://foo.com/bar.mp3' }
|
@@ -378,12 +379,16 @@ module Punchblock
|
|
378
379
|
}
|
379
380
|
end
|
380
381
|
|
381
|
-
it 'should playback the audio file using
|
382
|
-
|
382
|
+
it 'should playback the audio file using Playback' do
|
383
|
+
expect_answered
|
384
|
+
expect_playback
|
383
385
|
subject.execute
|
384
386
|
end
|
385
387
|
|
386
388
|
it 'should send a complete event when the file finishes playback' do
|
389
|
+
def mock_call.answered?
|
390
|
+
true
|
391
|
+
end
|
387
392
|
def mock_call.send_agi_action!(*args, &block)
|
388
393
|
block.call Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new(:code => 200, :result => 1)
|
389
394
|
end
|
@@ -400,18 +405,45 @@ module Punchblock
|
|
400
405
|
}
|
401
406
|
end
|
402
407
|
|
403
|
-
it 'should playback the audio file using
|
404
|
-
|
408
|
+
it 'should playback the audio file using Playback' do
|
409
|
+
expect_answered
|
410
|
+
expect_playback
|
405
411
|
subject.execute
|
406
412
|
end
|
407
413
|
|
408
414
|
it 'should send a complete event when the file finishes playback' do
|
415
|
+
expect_answered
|
409
416
|
def mock_call.send_agi_action!(*args, &block)
|
410
417
|
block.call Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new(:code => 200, :result => 1)
|
411
418
|
end
|
412
419
|
subject.execute
|
413
420
|
original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Success
|
414
421
|
end
|
422
|
+
|
423
|
+
context "with early media playback" do
|
424
|
+
it "should play the file with Playback" do
|
425
|
+
expect_answered false
|
426
|
+
expect_playback_noanswer
|
427
|
+
mock_call.expects(:send_progress)
|
428
|
+
subject.execute
|
429
|
+
end
|
430
|
+
|
431
|
+
context "with interrupt_on set to something that is not nil" do
|
432
|
+
let(:audio_filename) { 'tt-monkeys' }
|
433
|
+
let :command_options do
|
434
|
+
{
|
435
|
+
:ssml => RubySpeech::SSML.draw { string audio_filename },
|
436
|
+
:interrupt_on => :any
|
437
|
+
}
|
438
|
+
end
|
439
|
+
it "should return an error when the output is interruptible and it is early media" do
|
440
|
+
expect_answered false
|
441
|
+
error = ProtocolError.new.setup 'option error', 'Interrupt digits are not allowed with early media.'
|
442
|
+
subject.execute
|
443
|
+
original_command.response(0.1).should be == error
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|
415
447
|
end
|
416
448
|
|
417
449
|
context 'with a string (not SSML)' do
|
@@ -438,29 +470,27 @@ module Punchblock
|
|
438
470
|
}
|
439
471
|
end
|
440
472
|
|
441
|
-
it 'should playback
|
473
|
+
it 'should playback all audio files using Playback' do
|
442
474
|
latch = CountDownLatch.new 2
|
443
|
-
|
444
|
-
|
445
|
-
latch.countdown!
|
446
|
-
end
|
447
|
-
mock_call.expects(:send_agi_action!).once.with 'STREAM FILE', audio_filename2, nil do
|
448
|
-
subject.continue
|
449
|
-
latch.countdown!
|
450
|
-
end
|
475
|
+
expect_playback [audio_filename1, audio_filename2].join('&')
|
476
|
+
expect_answered
|
451
477
|
subject.execute
|
452
478
|
latch.wait 2
|
453
479
|
sleep 2
|
454
480
|
end
|
455
481
|
|
456
482
|
it 'should send a complete event after the final file has finished playback' do
|
483
|
+
expect_answered
|
457
484
|
def mock_call.send_agi_action!(*args, &block)
|
458
485
|
block.call Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new(:code => 200, :result => 1)
|
459
486
|
end
|
487
|
+
latch = CountDownLatch.new 1
|
460
488
|
original_command.expects(:add_event).once.with do |e|
|
461
489
|
e.reason.should be_a Punchblock::Component::Output::Complete::Success
|
490
|
+
latch.countdown!
|
462
491
|
end
|
463
492
|
subject.execute
|
493
|
+
latch.wait(2).should be_true
|
464
494
|
end
|
465
495
|
end
|
466
496
|
|
@@ -484,8 +514,9 @@ module Punchblock
|
|
484
514
|
describe 'start-offset' do
|
485
515
|
context 'unset' do
|
486
516
|
let(:command_opts) { { :start_offset => nil } }
|
487
|
-
it 'should not pass any options to
|
488
|
-
|
517
|
+
it 'should not pass any options to Playback' do
|
518
|
+
expect_answered
|
519
|
+
expect_playback
|
489
520
|
subject.execute
|
490
521
|
end
|
491
522
|
end
|
@@ -503,8 +534,9 @@ module Punchblock
|
|
503
534
|
describe 'start-paused' do
|
504
535
|
context 'false' do
|
505
536
|
let(:command_opts) { { :start_paused => false } }
|
506
|
-
it 'should not pass any options to
|
507
|
-
|
537
|
+
it 'should not pass any options to Playback' do
|
538
|
+
expect_answered
|
539
|
+
expect_playback
|
508
540
|
subject.execute
|
509
541
|
end
|
510
542
|
end
|
@@ -522,8 +554,9 @@ module Punchblock
|
|
522
554
|
describe 'repeat-interval' do
|
523
555
|
context 'unset' do
|
524
556
|
let(:command_opts) { { :repeat_interval => nil } }
|
525
|
-
it 'should not pass any options to
|
526
|
-
|
557
|
+
it 'should not pass any options to Playback' do
|
558
|
+
expect_answered
|
559
|
+
expect_playback
|
527
560
|
subject.execute
|
528
561
|
end
|
529
562
|
end
|
@@ -541,8 +574,9 @@ module Punchblock
|
|
541
574
|
describe 'repeat-times' do
|
542
575
|
context 'unset' do
|
543
576
|
let(:command_opts) { { :repeat_times => nil } }
|
544
|
-
it 'should not pass any options to
|
545
|
-
|
577
|
+
it 'should not pass any options to Playback' do
|
578
|
+
expect_answered
|
579
|
+
expect_playback
|
546
580
|
subject.execute
|
547
581
|
end
|
548
582
|
end
|
@@ -560,8 +594,9 @@ module Punchblock
|
|
560
594
|
describe 'max-time' do
|
561
595
|
context 'unset' do
|
562
596
|
let(:command_opts) { { :max_time => nil } }
|
563
|
-
it 'should not pass any options to
|
564
|
-
|
597
|
+
it 'should not pass any options to Playback' do
|
598
|
+
expect_answered
|
599
|
+
expect_playback
|
565
600
|
subject.execute
|
566
601
|
end
|
567
602
|
end
|
@@ -579,8 +614,9 @@ module Punchblock
|
|
579
614
|
describe 'voice' do
|
580
615
|
context 'unset' do
|
581
616
|
let(:command_opts) { { :voice => nil } }
|
582
|
-
it 'should not pass the v option to
|
583
|
-
|
617
|
+
it 'should not pass the v option to Playback' do
|
618
|
+
expect_answered
|
619
|
+
expect_playback
|
584
620
|
subject.execute
|
585
621
|
end
|
586
622
|
end
|
@@ -596,27 +632,98 @@ module Punchblock
|
|
596
632
|
end
|
597
633
|
|
598
634
|
describe 'interrupt_on' do
|
635
|
+
def ami_event_for_dtmf(digit, position)
|
636
|
+
RubyAMI::Event.new('DTMF').tap do |e|
|
637
|
+
e['Digit'] = digit.to_s
|
638
|
+
e['Start'] = position == :start ? 'Yes' : 'No'
|
639
|
+
e['End'] = position == :end ? 'Yes' : 'No'
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
def send_ami_events_for_dtmf(digit)
|
644
|
+
mock_call.process_ami_event ami_event_for_dtmf(digit, :start)
|
645
|
+
mock_call.process_ami_event ami_event_for_dtmf(digit, :end)
|
646
|
+
end
|
647
|
+
|
648
|
+
let(:reason) { original_command.complete_event(5).reason }
|
649
|
+
let(:channel) { "SIP/1234-00000000" }
|
650
|
+
let :ami_event do
|
651
|
+
RubyAMI::Event.new('AsyncAGI').tap do |e|
|
652
|
+
e['SubEvent'] = "Start"
|
653
|
+
e['Channel'] = channel
|
654
|
+
e['Env'] = "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2F1234-00000000%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201320835995.0%0Aagi_version%3A%201.8.4.1%0Aagi_callerid%3A%205678%0Aagi_calleridname%3A%20Jane%20Smith%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%201000%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20default%0Aagi_extension%3A%201000%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%204366221312%0A%0A"
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
599
658
|
context "set to nil" do
|
600
659
|
let(:command_opts) { { :interrupt_on => nil } }
|
601
|
-
it "
|
602
|
-
|
660
|
+
it "does not redirect the call" do
|
661
|
+
expect_answered
|
662
|
+
expect_playback
|
663
|
+
mock_call.expects(:redirect_back!).never
|
603
664
|
subject.execute
|
665
|
+
original_command.response(0.1).should be_a Ref
|
666
|
+
send_ami_events_for_dtmf 1
|
604
667
|
end
|
605
668
|
end
|
606
669
|
|
607
670
|
context "set to :any" do
|
608
671
|
let(:command_opts) { { :interrupt_on => :any } }
|
609
|
-
|
610
|
-
|
611
|
-
|
672
|
+
|
673
|
+
before do
|
674
|
+
expect_answered
|
675
|
+
expect_playback
|
676
|
+
end
|
677
|
+
|
678
|
+
context "when a DTMF digit is received" do
|
679
|
+
it "sends the correct complete event" do
|
680
|
+
mock_call.expects :redirect_back!
|
681
|
+
subject.execute
|
682
|
+
original_command.response(0.1).should be_a Ref
|
683
|
+
original_command.should_not be_complete
|
684
|
+
send_ami_events_for_dtmf 1
|
685
|
+
mock_call.process_ami_event! ami_event
|
686
|
+
sleep 0.2
|
687
|
+
original_command.should be_complete
|
688
|
+
reason.should be_a Punchblock::Component::Output::Complete::Success
|
689
|
+
end
|
690
|
+
|
691
|
+
it "redirects the call back to async AGI" do
|
692
|
+
mock_call.expects(:redirect_back!).with(nil).once
|
693
|
+
subject.execute
|
694
|
+
original_command.response(0.1).should be_a Ref
|
695
|
+
send_ami_events_for_dtmf 1
|
696
|
+
end
|
612
697
|
end
|
613
698
|
end
|
614
699
|
|
615
700
|
context "set to :dtmf" do
|
616
701
|
let(:command_opts) { { :interrupt_on => :dtmf } }
|
617
|
-
|
618
|
-
|
619
|
-
|
702
|
+
|
703
|
+
before do
|
704
|
+
expect_answered
|
705
|
+
expect_playback
|
706
|
+
end
|
707
|
+
|
708
|
+
context "when a DTMF digit is received" do
|
709
|
+
it "sends the correct complete event" do
|
710
|
+
mock_call.expects :redirect_back!
|
711
|
+
subject.execute
|
712
|
+
original_command.response(0.1).should be_a Ref
|
713
|
+
original_command.should_not be_complete
|
714
|
+
send_ami_events_for_dtmf 1
|
715
|
+
mock_call.process_ami_event! ami_event
|
716
|
+
sleep 0.2
|
717
|
+
original_command.should be_complete
|
718
|
+
reason.should be_a Punchblock::Component::Output::Complete::Success
|
719
|
+
end
|
720
|
+
|
721
|
+
it "redirects the call back to async AGI" do
|
722
|
+
mock_call.expects(:redirect_back!).with(nil).once
|
723
|
+
subject.execute
|
724
|
+
original_command.response(0.1).should be_a Ref
|
725
|
+
send_ami_events_for_dtmf 1
|
726
|
+
end
|
620
727
|
end
|
621
728
|
end
|
622
729
|
|