punchblock 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.travis.yml +3 -3
  2. data/CHANGELOG.md +23 -0
  3. data/lib/punchblock.rb +24 -0
  4. data/lib/punchblock/command/reject.rb +10 -2
  5. data/lib/punchblock/component/record.rb +16 -0
  6. data/lib/punchblock/core_ext/blather/stanza.rb +3 -1
  7. data/lib/punchblock/dead_actor_safety.rb +9 -0
  8. data/lib/punchblock/event/complete.rb +9 -11
  9. data/lib/punchblock/rayo_node.rb +4 -0
  10. data/lib/punchblock/translator/asterisk.rb +65 -22
  11. data/lib/punchblock/translator/asterisk/call.rb +49 -30
  12. data/lib/punchblock/translator/asterisk/component.rb +6 -8
  13. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +13 -20
  14. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +1 -1
  15. data/lib/punchblock/translator/asterisk/component/input.rb +3 -6
  16. data/lib/punchblock/translator/asterisk/component/output.rb +40 -45
  17. data/lib/punchblock/translator/asterisk/component/record.rb +1 -1
  18. data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +5 -2
  19. data/lib/punchblock/version.rb +1 -1
  20. data/punchblock.gemspec +5 -5
  21. data/spec/punchblock/command/reject_spec.rb +7 -1
  22. data/spec/punchblock/command_node_spec.rb +5 -2
  23. data/spec/punchblock/component/component_node_spec.rb +4 -0
  24. data/spec/punchblock/component/output_spec.rb +1 -1
  25. data/spec/punchblock/component/record_spec.rb +30 -0
  26. data/spec/punchblock/event/complete_spec.rb +10 -0
  27. data/spec/punchblock/translator/asterisk/call_spec.rb +191 -48
  28. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +6 -39
  29. data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +3 -3
  30. data/spec/punchblock/translator/asterisk/component/input_spec.rb +8 -3
  31. data/spec/punchblock/translator/asterisk/component/output_spec.rb +153 -46
  32. data/spec/punchblock/translator/asterisk/component/record_spec.rb +6 -5
  33. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +1 -2
  34. data/spec/punchblock/translator/asterisk/component_spec.rb +1 -0
  35. data/spec/punchblock/translator/asterisk_spec.rb +147 -12
  36. data/spec/punchblock_spec.rb +34 -0
  37. data/spec/spec_helper.rb +5 -1
  38. 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) { UUIDTools::UUID.random_create }
19
+ let(:component_id) { Punchblock.new_uuid }
20
20
 
21
- before { UUIDTools::UUID.stubs :random_create => component_id }
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!).once.with(expected_action).returns(expected_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!).once.with(expected_action).returns(expected_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) { UUIDTools::UUID.random_create }
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 { UUIDTools::UUID.stubs :random_create => component_id }
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!).once.with(expected_action).returns(expected_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 answer_if_not_answered on the call" do
46
- call.expects :answer_if_not_answered
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 :answer_if_not_answered }
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 expect_stream_file_with_options(options = nil)
338
- mock_call.expects(:send_agi_action!).once.with 'STREAM FILE', audio_filename, options do |*args|
339
- args[2].should be == options
340
- subject.continue!
341
- true
342
- end
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 STREAM FILE' do
382
- expect_stream_file_with_options
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 STREAM FILE' do
404
- expect_stream_file_with_options
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 each audio file using STREAM FILE' do
473
+ it 'should playback all audio files using Playback' do
442
474
  latch = CountDownLatch.new 2
443
- mock_call.expects(:send_agi_action!).once.with 'STREAM FILE', audio_filename1, nil do
444
- subject.continue
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 STREAM FILE' do
488
- expect_stream_file_with_options
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 STREAM FILE' do
507
- expect_stream_file_with_options
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 STREAM FILE' do
526
- expect_stream_file_with_options
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 STREAM FILE' do
545
- expect_stream_file_with_options
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 STREAM FILE' do
564
- expect_stream_file_with_options
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 STREAM FILE' do
583
- expect_stream_file_with_options
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 "should not pass any digits to STREAM FILE" do
602
- expect_stream_file_with_options
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
- it "should pass all digits to STREAM FILE" do
610
- expect_stream_file_with_options '0123456789*#'
611
- subject.execute
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
- it "should pass all digits to STREAM FILE" do
618
- expect_stream_file_with_options '0123456789*#'
619
- subject.execute
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