punchblock 1.2.0 → 1.3.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 (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