punchblock 1.1.0 → 1.2.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.
- data/CHANGELOG.md +3 -0
- data/lib/punchblock/component/record.rb +4 -0
- data/lib/punchblock/translator/asterisk/call.rb +5 -0
- data/lib/punchblock/translator/asterisk/component.rb +11 -2
- data/lib/punchblock/translator/asterisk/component/record.rb +104 -0
- data/lib/punchblock/version.rb +1 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +33 -3
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +2 -2
- data/spec/punchblock/translator/asterisk/component/record_spec.rb +421 -0
- data/spec/punchblock/translator/asterisk/component_spec.rb +7 -0
- metadata +6 -3
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# [develop](https://github.com/adhearsion/punchblock)
|
|
2
2
|
|
|
3
|
+
# [v1.2.0](https://github.com/adhearsion/punchblock/compare/v1.1.0...v1.2.0) - [2012-04-29](https://rubygems.org/gems/punchblock/versions/1.2.0)
|
|
4
|
+
* Feature: Basic support for record component on Asterisk, using MixMonitor. Currently unsupported options include: start_paused, initial_timeout, final_timeout. Hints are additionally not supported, and recordings are stored on the * machine's local filesystem.
|
|
5
|
+
|
|
3
6
|
# [v1.1.0](https://github.com/adhearsion/punchblock/compare/v1.0.0...v1.1.0) - [2012-04-26](https://rubygems.org/gems/punchblock/versions/1.1.0)
|
|
4
7
|
* Feature: Implement Reject on Asterisk
|
|
5
8
|
* Bugfix: No longer generate warnings
|
|
@@ -110,6 +110,9 @@ module Punchblock
|
|
|
110
110
|
case ami_event.name
|
|
111
111
|
when 'Hangup'
|
|
112
112
|
pb_logger.trace "Received a Hangup AMI event. Sending End event."
|
|
113
|
+
@components.dup.each_pair do |id, component|
|
|
114
|
+
component.call_ended if component.alive?
|
|
115
|
+
end
|
|
113
116
|
send_end_event HANGUP_CAUSE_TO_END_REASON[ami_event['Cause'].to_i]
|
|
114
117
|
when 'AsyncAGI'
|
|
115
118
|
pb_logger.trace "Received an AsyncAGI event. Looking for matching AGICommand component."
|
|
@@ -213,6 +216,8 @@ module Punchblock
|
|
|
213
216
|
execute_component Component::Output, command
|
|
214
217
|
when Punchblock::Component::Input
|
|
215
218
|
execute_component Component::Input, command
|
|
219
|
+
when Punchblock::Component::Record
|
|
220
|
+
execute_component Component::Record, command
|
|
216
221
|
else
|
|
217
222
|
command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command for call #{id}", id
|
|
218
223
|
end
|
|
@@ -9,6 +9,7 @@ module Punchblock
|
|
|
9
9
|
autoload :Asterisk
|
|
10
10
|
autoload :Input
|
|
11
11
|
autoload :Output
|
|
12
|
+
autoload :Record
|
|
12
13
|
autoload :StopByRedirect
|
|
13
14
|
|
|
14
15
|
class Component
|
|
@@ -22,6 +23,7 @@ module Punchblock
|
|
|
22
23
|
def initialize(component_node, call = nil)
|
|
23
24
|
@component_node, @call = component_node, call
|
|
24
25
|
@id = UUIDTools::UUID.random_create.to_s
|
|
26
|
+
@complete = false
|
|
25
27
|
setup
|
|
26
28
|
pb_logger.debug "Starting up..."
|
|
27
29
|
end
|
|
@@ -33,9 +35,12 @@ module Punchblock
|
|
|
33
35
|
command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command for component #{id}", call_id, id
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
def send_complete_event(reason)
|
|
38
|
+
def send_complete_event(reason, recording = nil)
|
|
39
|
+
return if @complete
|
|
40
|
+
@complete = true
|
|
37
41
|
event = Punchblock::Event::Complete.new.tap do |c|
|
|
38
42
|
c.reason = reason
|
|
43
|
+
c << recording if recording
|
|
39
44
|
end
|
|
40
45
|
send_event event
|
|
41
46
|
current_actor.terminate!
|
|
@@ -48,7 +53,7 @@ module Punchblock
|
|
|
48
53
|
if internal
|
|
49
54
|
@component_node.add_event event
|
|
50
55
|
else
|
|
51
|
-
translator.
|
|
56
|
+
translator.handle_pb_event! event
|
|
52
57
|
end
|
|
53
58
|
end
|
|
54
59
|
|
|
@@ -60,6 +65,10 @@ module Punchblock
|
|
|
60
65
|
call.id if call
|
|
61
66
|
end
|
|
62
67
|
|
|
68
|
+
def call_ended
|
|
69
|
+
send_complete_event Punchblock::Event::Complete::Hangup.new
|
|
70
|
+
end
|
|
71
|
+
|
|
63
72
|
private
|
|
64
73
|
|
|
65
74
|
def translator
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module Punchblock
|
|
4
|
+
module Translator
|
|
5
|
+
class Asterisk
|
|
6
|
+
module Component
|
|
7
|
+
class Record < Component
|
|
8
|
+
RECORDING_BASE_PATH = '/var/punchblock/record'
|
|
9
|
+
|
|
10
|
+
def setup
|
|
11
|
+
@complete_reason = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute
|
|
15
|
+
max_duration = @component_node.max_duration || -1
|
|
16
|
+
|
|
17
|
+
raise OptionError, 'A start-paused value of true is unsupported.' if @component_node.start_paused
|
|
18
|
+
raise OptionError, 'An initial-timeout value is unsupported.' if @component_node.initial_timeout && @component_node.initial_timeout != -1
|
|
19
|
+
raise OptionError, 'A final-timeout value is unsupported.' if @component_node.final_timeout && @component_node.final_timeout != -1
|
|
20
|
+
raise OptionError, 'A max-duration value that is negative (and not -1) is invalid.' unless max_duration >= -1
|
|
21
|
+
|
|
22
|
+
@format = @component_node.format || 'wav'
|
|
23
|
+
|
|
24
|
+
@call.answer_if_not_answered
|
|
25
|
+
|
|
26
|
+
component = current_actor
|
|
27
|
+
call.register_handler :ami, :name => 'MonitorStop' do |event|
|
|
28
|
+
component.finished
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
send_ref
|
|
32
|
+
|
|
33
|
+
if @component_node.start_beep
|
|
34
|
+
pb_logger.debug "Playing a beep via STREAM FILE before recording"
|
|
35
|
+
@call.send_agi_action! 'STREAM FILE', 'beep', '""' do
|
|
36
|
+
component.signal! :beep_finished
|
|
37
|
+
end
|
|
38
|
+
wait :beep_finished
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
call.send_ami_action! 'Monitor', 'Channel' => call.channel, 'File' => filename, 'Format' => @format, 'Mix' => true
|
|
42
|
+
unless max_duration == -1
|
|
43
|
+
after max_duration/1000 do
|
|
44
|
+
pb_logger.trace "Max duration encountered, stopping recording"
|
|
45
|
+
call.send_ami_action! 'StopMonitor', 'Channel' => call.channel
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
rescue OptionError => e
|
|
49
|
+
with_error 'option error', e.message
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def execute_command(command)
|
|
53
|
+
case command
|
|
54
|
+
when Punchblock::Component::Stop
|
|
55
|
+
command.response = true
|
|
56
|
+
a = current_actor
|
|
57
|
+
call.send_ami_action! 'StopMonitor', 'Channel' => call.channel do |complete_event|
|
|
58
|
+
@complete_reason = stop_reason
|
|
59
|
+
end
|
|
60
|
+
when Punchblock::Component::Record::Pause
|
|
61
|
+
a = current_actor
|
|
62
|
+
call.send_ami_action! 'PauseMonitor', 'Channel' => call.channel do |complete_event|
|
|
63
|
+
command.response = true
|
|
64
|
+
end
|
|
65
|
+
when Punchblock::Component::Record::Resume
|
|
66
|
+
a = current_actor
|
|
67
|
+
call.send_ami_action! 'ResumeMonitor', 'Channel' => call.channel do |complete_event|
|
|
68
|
+
command.response = true
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def finished
|
|
76
|
+
send_complete_event(@complete_reason || success_reason)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def filename
|
|
82
|
+
File.join RECORDING_BASE_PATH, id
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def recording
|
|
86
|
+
Punchblock::Component::Record::Recording.new :uri => "file://#{filename}.#{@format}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def stop_reason
|
|
90
|
+
Punchblock::Event::Complete::Stop.new
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def success_reason
|
|
94
|
+
Punchblock::Component::Record::Complete::Success.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def send_complete_event(reason)
|
|
98
|
+
super reason, recording
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/punchblock/version.rb
CHANGED
|
@@ -235,6 +235,21 @@ module Punchblock
|
|
|
235
235
|
subject.should_not be_alive
|
|
236
236
|
end
|
|
237
237
|
|
|
238
|
+
it "should cause all components to send complete events before sending end event" do
|
|
239
|
+
subject.expects :answer_if_not_answered
|
|
240
|
+
comp_command = Punchblock::Component::Input.new :grammar => {:value => '<grammar/>'}, :mode => :dtmf
|
|
241
|
+
comp_command.request!
|
|
242
|
+
component = subject.execute_command comp_command
|
|
243
|
+
comp_command.response(0.1).should be_a Ref
|
|
244
|
+
expected_complete_event = Punchblock::Event::Complete.new :target_call_id => subject.id, :component_id => component.id
|
|
245
|
+
expected_complete_event.reason = Punchblock::Event::Complete::Hangup.new
|
|
246
|
+
expected_end_event = Punchblock::Event::End.new :reason => :hangup, :target_call_id => subject.id
|
|
247
|
+
end_sequence = sequence 'end events'
|
|
248
|
+
translator.expects(:handle_pb_event!).with(expected_complete_event).once.in_sequence(end_sequence)
|
|
249
|
+
translator.expects(:handle_pb_event!).with(expected_end_event).once.in_sequence(end_sequence)
|
|
250
|
+
subject.process_ami_event ami_event
|
|
251
|
+
end
|
|
252
|
+
|
|
238
253
|
context "with an undefined cause" do
|
|
239
254
|
let(:cause) { '0' }
|
|
240
255
|
let(:cause_txt) { 'Undefined' }
|
|
@@ -736,7 +751,7 @@ module Punchblock
|
|
|
736
751
|
|
|
737
752
|
let(:mock_action) { mock 'Component::Asterisk::Output', :id => 'foo' }
|
|
738
753
|
|
|
739
|
-
it 'should create an
|
|
754
|
+
it 'should create an Output component and execute it asynchronously' do
|
|
740
755
|
Component::Output.expects(:new).once.with(command, subject).returns mock_action
|
|
741
756
|
mock_action.expects(:internal=).never
|
|
742
757
|
mock_action.expects(:execute!).once
|
|
@@ -751,7 +766,7 @@ module Punchblock
|
|
|
751
766
|
|
|
752
767
|
let(:mock_action) { mock 'Component::Asterisk::Input', :id => 'foo' }
|
|
753
768
|
|
|
754
|
-
it 'should create an
|
|
769
|
+
it 'should create an Input component and execute it asynchronously' do
|
|
755
770
|
Component::Input.expects(:new).once.with(command, subject).returns mock_action
|
|
756
771
|
mock_action.expects(:internal=).never
|
|
757
772
|
mock_action.expects(:execute!).once
|
|
@@ -759,6 +774,21 @@ module Punchblock
|
|
|
759
774
|
end
|
|
760
775
|
end
|
|
761
776
|
|
|
777
|
+
context 'with a Record component' do
|
|
778
|
+
let :command do
|
|
779
|
+
Punchblock::Component::Record.new
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
let(:mock_action) { mock 'Component::Asterisk::Record', :id => 'foo' }
|
|
783
|
+
|
|
784
|
+
it 'should create a Record component and execute it asynchronously' do
|
|
785
|
+
Component::Record.expects(:new).once.with(command, subject).returns mock_action
|
|
786
|
+
mock_action.expects(:internal=).never
|
|
787
|
+
mock_action.expects(:execute!).once
|
|
788
|
+
subject.execute_command command
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
|
|
762
792
|
context 'with a component command' do
|
|
763
793
|
let(:component_id) { 'foobar' }
|
|
764
794
|
|
|
@@ -789,7 +819,7 @@ module Punchblock
|
|
|
789
819
|
|
|
790
820
|
context 'with a command we do not understand' do
|
|
791
821
|
let :command do
|
|
792
|
-
Punchblock::
|
|
822
|
+
Punchblock::Command::Mute.new
|
|
793
823
|
end
|
|
794
824
|
|
|
795
825
|
it 'sends an error in response to the command' do
|
|
@@ -116,10 +116,10 @@ module Punchblock
|
|
|
116
116
|
it 'should send a complete event' do
|
|
117
117
|
subject.handle_ami_event ami_event
|
|
118
118
|
|
|
119
|
-
command.should be_complete
|
|
120
|
-
|
|
121
119
|
complete_event = command.complete_event 0.5
|
|
122
120
|
|
|
121
|
+
command.should be_complete
|
|
122
|
+
|
|
123
123
|
complete_event.component_id.should be == component_id.to_s
|
|
124
124
|
complete_event.reason.should be == expected_complete_reason
|
|
125
125
|
end
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
module Punchblock
|
|
6
|
+
module Translator
|
|
7
|
+
class Asterisk
|
|
8
|
+
module Component
|
|
9
|
+
describe Record do
|
|
10
|
+
let(:connection) do
|
|
11
|
+
mock_connection_with_event_handler do |event|
|
|
12
|
+
original_command.add_event event
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
let(:media_engine) { nil }
|
|
16
|
+
let(:channel) { 'SIP/foo' }
|
|
17
|
+
let(:translator) { Punchblock::Translator::Asterisk.new mock('AMI'), connection, media_engine }
|
|
18
|
+
let(:mock_call) { Punchblock::Translator::Asterisk::Call.new channel, translator }
|
|
19
|
+
|
|
20
|
+
let :original_command do
|
|
21
|
+
Punchblock::Component::Record.new command_options
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
let :command_options do
|
|
25
|
+
{}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
subject { Record.new original_command, mock_call }
|
|
29
|
+
|
|
30
|
+
describe '#execute' do
|
|
31
|
+
let(:reason) { original_command.complete_event(5).reason }
|
|
32
|
+
let(:recording) { original_command.complete_event(5).recording }
|
|
33
|
+
|
|
34
|
+
before { original_command.request! }
|
|
35
|
+
|
|
36
|
+
it "calls answer_if_not_answered on the call" do
|
|
37
|
+
mock_call.expects :send_ami_action!
|
|
38
|
+
mock_call.expects :answer_if_not_answered
|
|
39
|
+
subject.execute
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
before { mock_call.stubs :answer_if_not_answered }
|
|
43
|
+
|
|
44
|
+
it "sets command response to a reference to the component" do
|
|
45
|
+
mock_call.expects(:send_ami_action!)
|
|
46
|
+
subject.execute
|
|
47
|
+
original_command.response(0.1).should be_a Ref
|
|
48
|
+
original_command.component_id.should be == subject.id
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "starts a recording via AMI, using the component ID as the filename" do
|
|
52
|
+
filename = "#{Record::RECORDING_BASE_PATH}/#{subject.id}"
|
|
53
|
+
mock_call.expects(:send_ami_action!).once.with('Monitor', 'Channel' => channel, 'File' => filename, 'Format' => 'wav', 'Mix' => true)
|
|
54
|
+
subject.execute
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "sends a success complete event when the recording ends" do
|
|
58
|
+
full_filename = "file://#{Record::RECORDING_BASE_PATH}/#{subject.id}.wav"
|
|
59
|
+
mock_call.expects(:send_ami_action!)
|
|
60
|
+
subject.execute
|
|
61
|
+
monitor_stop_event = RubyAMI::Event.new('MonitorStop').tap do |e|
|
|
62
|
+
e['Channel'] = channel
|
|
63
|
+
end
|
|
64
|
+
mock_call.process_ami_event monitor_stop_event
|
|
65
|
+
reason.should be_a Punchblock::Component::Record::Complete::Success
|
|
66
|
+
recording.uri.should be == full_filename
|
|
67
|
+
original_command.should be_complete
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe 'start_paused' do
|
|
71
|
+
context "set to nil" do
|
|
72
|
+
let(:command_options) { { :start_paused => nil } }
|
|
73
|
+
it "should execute normally" do
|
|
74
|
+
mock_call.expects(:send_ami_action!).once
|
|
75
|
+
subject.execute
|
|
76
|
+
original_command.response(0.1).should be_a Ref
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
context "set to false" do
|
|
81
|
+
let(:command_options) { { :start_paused => false } }
|
|
82
|
+
it "should execute normally" do
|
|
83
|
+
mock_call.expects(:send_ami_action!).once
|
|
84
|
+
subject.execute
|
|
85
|
+
original_command.response(0.1).should be_a Ref
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context "set to true" do
|
|
90
|
+
let(:command_options) { { :start_paused => true } }
|
|
91
|
+
it "should return an error and not execute any actions" do
|
|
92
|
+
mock_call.expects(:send_agi_action!).never
|
|
93
|
+
subject.execute
|
|
94
|
+
error = ProtocolError.new.setup 'option error', 'A start-paused value of true is unsupported.'
|
|
95
|
+
original_command.response(0.1).should be == error
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe 'initial_timeout' do
|
|
101
|
+
context "set to nil" do
|
|
102
|
+
let(:command_options) { { :initial_timeout => nil } }
|
|
103
|
+
it "should execute normally" do
|
|
104
|
+
mock_call.expects(:send_ami_action!).once
|
|
105
|
+
subject.execute
|
|
106
|
+
original_command.response(0.1).should be_a Ref
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
context "set to -1" do
|
|
111
|
+
let(:command_options) { { :initial_timeout => -1 } }
|
|
112
|
+
it "should execute normally" do
|
|
113
|
+
mock_call.expects(:send_ami_action!).once
|
|
114
|
+
subject.execute
|
|
115
|
+
original_command.response(0.1).should be_a Ref
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
context "set to a positive number" do
|
|
120
|
+
let(:command_options) { { :initial_timeout => 10 } }
|
|
121
|
+
it "should return an error and not execute any actions" do
|
|
122
|
+
mock_call.expects(:send_agi_action!).never
|
|
123
|
+
subject.execute
|
|
124
|
+
error = ProtocolError.new.setup 'option error', 'An initial-timeout value is unsupported.'
|
|
125
|
+
original_command.response(0.1).should be == error
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe 'final_timeout' do
|
|
131
|
+
context "set to nil" do
|
|
132
|
+
let(:command_options) { { :final_timeout => nil } }
|
|
133
|
+
it "should execute normally" do
|
|
134
|
+
mock_call.expects(:send_ami_action!).once
|
|
135
|
+
subject.execute
|
|
136
|
+
original_command.response(0.1).should be_a Ref
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
context "set to -1" do
|
|
141
|
+
let(:command_options) { { :final_timeout => -1 } }
|
|
142
|
+
it "should execute normally" do
|
|
143
|
+
mock_call.expects(:send_ami_action!).once
|
|
144
|
+
subject.execute
|
|
145
|
+
original_command.response(0.1).should be_a Ref
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
context "set to a positive number" do
|
|
150
|
+
let(:command_options) { { :final_timeout => 10 } }
|
|
151
|
+
it "should return an error and not execute any actions" do
|
|
152
|
+
mock_call.expects(:send_agi_action!).never
|
|
153
|
+
subject.execute
|
|
154
|
+
error = ProtocolError.new.setup 'option error', 'A final-timeout value is unsupported.'
|
|
155
|
+
original_command.response(0.1).should be == error
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
describe 'format' do
|
|
161
|
+
context "set to nil" do
|
|
162
|
+
let(:command_options) { { :format => nil } }
|
|
163
|
+
it "should execute as 'wav'" do
|
|
164
|
+
mock_call.expects(:send_ami_action!).once.with('Monitor', has_entry('Format' => 'wav'))
|
|
165
|
+
subject.execute
|
|
166
|
+
original_command.response(0.1).should be_a Ref
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "provides the correct filename in the recording" do
|
|
170
|
+
mock_call.expects(:send_ami_action!)
|
|
171
|
+
subject.execute
|
|
172
|
+
monitor_stop_event = RubyAMI::Event.new('MonitorStop').tap do |e|
|
|
173
|
+
e['Channel'] = channel
|
|
174
|
+
end
|
|
175
|
+
mock_call.process_ami_event monitor_stop_event
|
|
176
|
+
recording.uri.should match(/.*\.wav$/)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
context "set to 'mp3'" do
|
|
181
|
+
let(:command_options) { { :format => 'mp3' } }
|
|
182
|
+
it "should execute as 'mp3'" do
|
|
183
|
+
mock_call.expects(:send_ami_action!).once.with('Monitor', has_entry('Format' => 'mp3'))
|
|
184
|
+
subject.execute
|
|
185
|
+
original_command.response(0.1).should be_a Ref
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "provides the correct filename in the recording" do
|
|
189
|
+
mock_call.expects(:send_ami_action!)
|
|
190
|
+
subject.execute
|
|
191
|
+
monitor_stop_event = RubyAMI::Event.new('MonitorStop').tap do |e|
|
|
192
|
+
e['Channel'] = channel
|
|
193
|
+
end
|
|
194
|
+
mock_call.process_ami_event monitor_stop_event
|
|
195
|
+
recording.uri.should match(/.*\.mp3$/)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
describe 'start_beep' do
|
|
201
|
+
context "set to nil" do
|
|
202
|
+
let(:command_options) { { :start_beep => nil } }
|
|
203
|
+
it "should execute normally" do
|
|
204
|
+
mock_call.expects(:send_agi_action!).never.with('STREAM FILE', 'beep', '""')
|
|
205
|
+
mock_call.expects(:send_ami_action!).once
|
|
206
|
+
subject.execute
|
|
207
|
+
original_command.response(0.1).should be_a Ref
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
context "set to false" do
|
|
212
|
+
let(:command_options) { { :start_beep => false } }
|
|
213
|
+
it "should execute normally" do
|
|
214
|
+
mock_call.expects(:send_agi_action!).never.with('STREAM FILE', 'beep', '""')
|
|
215
|
+
mock_call.expects(:send_ami_action!).once
|
|
216
|
+
subject.execute
|
|
217
|
+
original_command.response(0.1).should be_a Ref
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
context "set to true" do
|
|
222
|
+
let(:command_options) { { :start_beep => true } }
|
|
223
|
+
|
|
224
|
+
it "should play a beep before recording" do
|
|
225
|
+
execute_seq = sequence 'beep then record'
|
|
226
|
+
subject.wrapped_object.expects(:wait).once
|
|
227
|
+
mock_call.expects(:send_agi_action!).once.with('STREAM FILE', 'beep', '""').in_sequence(execute_seq)
|
|
228
|
+
mock_call.expects(:send_ami_action!).once.in_sequence(execute_seq)
|
|
229
|
+
subject.execute
|
|
230
|
+
original_command.response(0.1).should be_a Ref
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "should wait for the beep to finish before starting recording" do
|
|
234
|
+
def mock_call.send_agi_action!(*args)
|
|
235
|
+
yield
|
|
236
|
+
end
|
|
237
|
+
mock_call.expects(:send_ami_action!).once
|
|
238
|
+
subject.execute
|
|
239
|
+
original_command.response(0.1).should be_a Ref
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
describe 'max_duration' do
|
|
245
|
+
context "set to nil" do
|
|
246
|
+
let(:command_options) { { :max_duration => nil } }
|
|
247
|
+
it "should execute normally" do
|
|
248
|
+
mock_call.expects(:send_ami_action!).once
|
|
249
|
+
subject.execute
|
|
250
|
+
original_command.response(0.1).should be_a Ref
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
context "set to -1" do
|
|
255
|
+
let(:command_options) { { :max_duration => -1 } }
|
|
256
|
+
it "should execute normally" do
|
|
257
|
+
mock_call.expects(:send_ami_action!).once
|
|
258
|
+
subject.execute
|
|
259
|
+
original_command.response(0.1).should be_a Ref
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
context 'a negative number other than -1' do
|
|
264
|
+
let(:command_options) { { :max_duration => -1000 } }
|
|
265
|
+
|
|
266
|
+
it "should return an error and not execute any actions" do
|
|
267
|
+
subject.execute
|
|
268
|
+
error = ProtocolError.new.setup 'option error', 'A max-duration value that is negative (and not -1) is invalid.'
|
|
269
|
+
original_command.response(0.1).should be == error
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
context 'a positive number' do
|
|
274
|
+
let(:reason) { original_command.complete_event(5).reason }
|
|
275
|
+
let(:recording) { original_command.complete_event(5).recording }
|
|
276
|
+
let(:command_options) { { :max_duration => 1000 } }
|
|
277
|
+
|
|
278
|
+
it "executes a StopMonitor action" do
|
|
279
|
+
mock_call.expects :send_ami_action!
|
|
280
|
+
mock_call.expects(:send_ami_action!).once.with('StopMonitor', 'Channel' => channel)
|
|
281
|
+
subject.execute
|
|
282
|
+
sleep 1.2
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it "sends the correct complete event" do
|
|
286
|
+
def mock_call.send_ami_action!(*args, &block)
|
|
287
|
+
block.call Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new if block
|
|
288
|
+
end
|
|
289
|
+
full_filename = "file://#{Record::RECORDING_BASE_PATH}/#{subject.id}.wav"
|
|
290
|
+
subject.execute
|
|
291
|
+
sleep 1.2
|
|
292
|
+
|
|
293
|
+
monitor_stop_event = RubyAMI::Event.new('MonitorStop').tap do |e|
|
|
294
|
+
e['Channel'] = channel
|
|
295
|
+
end
|
|
296
|
+
mock_call.process_ami_event monitor_stop_event
|
|
297
|
+
|
|
298
|
+
reason.should be_a Punchblock::Component::Record::Complete::Success
|
|
299
|
+
recording.uri.should be == full_filename
|
|
300
|
+
original_command.should be_complete
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
describe "#execute_command" do
|
|
307
|
+
let(:reason) { original_command.complete_event(5).reason }
|
|
308
|
+
let(:recording) { original_command.complete_event(5).recording }
|
|
309
|
+
|
|
310
|
+
context "with a command it does not understand" do
|
|
311
|
+
let(:command) { Punchblock::Component::Output::Pause.new }
|
|
312
|
+
|
|
313
|
+
before { command.request! }
|
|
314
|
+
it "returns a ProtocolError response" do
|
|
315
|
+
subject.execute_command command
|
|
316
|
+
command.response(0.1).should be_a ProtocolError
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
context "with a Stop command" do
|
|
321
|
+
let(:command) { Punchblock::Component::Stop.new }
|
|
322
|
+
|
|
323
|
+
before do
|
|
324
|
+
mock_call.expects :answer_if_not_answered
|
|
325
|
+
mock_call.expects :send_ami_action!
|
|
326
|
+
command.request!
|
|
327
|
+
original_command.request!
|
|
328
|
+
subject.execute
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
let :send_stop_event do
|
|
332
|
+
monitor_stop_event = RubyAMI::Event.new('MonitorStop').tap do |e|
|
|
333
|
+
e['Channel'] = channel
|
|
334
|
+
end
|
|
335
|
+
mock_call.process_ami_event monitor_stop_event
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it "sets the command response to true" do
|
|
339
|
+
mock_call.expects :send_ami_action!
|
|
340
|
+
subject.execute_command command
|
|
341
|
+
send_stop_event
|
|
342
|
+
command.response(0.1).should be == true
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
it "executes a StopMonitor action" do
|
|
346
|
+
mock_call.expects(:send_ami_action!).once.with('StopMonitor', 'Channel' => channel)
|
|
347
|
+
subject.execute_command command
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
it "sends the correct complete event" do
|
|
351
|
+
mock_call.instance_exec do
|
|
352
|
+
class << self
|
|
353
|
+
undef :send_ami_action! # This is here because mocha has already defined #send_ami_action! above. We need to undef it to prevent a warning on redefinition.
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def send_ami_action!(*args, &block)
|
|
357
|
+
block.call Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new if block
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
full_filename = "file://#{Record::RECORDING_BASE_PATH}/#{subject.id}.wav"
|
|
362
|
+
subject.execute_command command
|
|
363
|
+
send_stop_event
|
|
364
|
+
reason.should be_a Punchblock::Event::Complete::Stop
|
|
365
|
+
recording.uri.should be == full_filename
|
|
366
|
+
original_command.should be_complete
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
context "with a Pause command" do
|
|
371
|
+
let(:command) { Punchblock::Component::Record::Pause.new }
|
|
372
|
+
|
|
373
|
+
before do
|
|
374
|
+
command.request!
|
|
375
|
+
original_command.request!
|
|
376
|
+
original_command.execute!
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it "sets the command response to true" do
|
|
380
|
+
def mock_call.send_ami_action!(*args, &block)
|
|
381
|
+
block.call Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new if block
|
|
382
|
+
end
|
|
383
|
+
subject.execute_command command
|
|
384
|
+
command.response(0.1).should be == true
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it "pauses the recording via AMI" do
|
|
388
|
+
mock_call.expects(:send_ami_action!).once.with('PauseMonitor', 'Channel' => channel)
|
|
389
|
+
subject.execute_command command
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
context "with a Resume command" do
|
|
394
|
+
let(:command) { Punchblock::Component::Record::Resume.new }
|
|
395
|
+
|
|
396
|
+
before do
|
|
397
|
+
command.request!
|
|
398
|
+
original_command.request!
|
|
399
|
+
original_command.execute!
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
it "sets the command response to true" do
|
|
403
|
+
def mock_call.send_ami_action!(*args, &block)
|
|
404
|
+
block.call Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new if block
|
|
405
|
+
end
|
|
406
|
+
subject.execute_command command
|
|
407
|
+
command.response(0.1).should be == true
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it "resumes the recording via AMI" do
|
|
411
|
+
mock_call.expects(:send_ami_action!).once.with('ResumeMonitor', 'Channel' => channel)
|
|
412
|
+
subject.execute_command command
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
@@ -71,6 +71,13 @@ module Punchblock
|
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
describe "#call_ended" do
|
|
75
|
+
it "should send a complete event with the call hangup reason" do
|
|
76
|
+
subject.wrapped_object.expects(:send_complete_event).once.with Punchblock::Event::Complete::Hangup.new
|
|
77
|
+
subject.call_ended
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
74
81
|
describe '#execute_command' do
|
|
75
82
|
before do
|
|
76
83
|
component_command.request!
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: punchblock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -11,7 +11,7 @@ authors:
|
|
|
11
11
|
autorequire:
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date: 2012-04-
|
|
14
|
+
date: 2012-04-29 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: niceogiri
|
|
@@ -401,6 +401,7 @@ files:
|
|
|
401
401
|
- lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb
|
|
402
402
|
- lib/punchblock/translator/asterisk/component/input.rb
|
|
403
403
|
- lib/punchblock/translator/asterisk/component/output.rb
|
|
404
|
+
- lib/punchblock/translator/asterisk/component/record.rb
|
|
404
405
|
- lib/punchblock/translator/asterisk/component/stop_by_redirect.rb
|
|
405
406
|
- lib/punchblock/version.rb
|
|
406
407
|
- punchblock.gemspec
|
|
@@ -445,6 +446,7 @@ files:
|
|
|
445
446
|
- spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb
|
|
446
447
|
- spec/punchblock/translator/asterisk/component/input_spec.rb
|
|
447
448
|
- spec/punchblock/translator/asterisk/component/output_spec.rb
|
|
449
|
+
- spec/punchblock/translator/asterisk/component/record_spec.rb
|
|
448
450
|
- spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb
|
|
449
451
|
- spec/punchblock/translator/asterisk/component_spec.rb
|
|
450
452
|
- spec/punchblock/translator/asterisk_spec.rb
|
|
@@ -465,7 +467,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
465
467
|
version: '0'
|
|
466
468
|
segments:
|
|
467
469
|
- 0
|
|
468
|
-
hash:
|
|
470
|
+
hash: 621112647086219314
|
|
469
471
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
470
472
|
none: false
|
|
471
473
|
requirements:
|
|
@@ -520,6 +522,7 @@ test_files:
|
|
|
520
522
|
- spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb
|
|
521
523
|
- spec/punchblock/translator/asterisk/component/input_spec.rb
|
|
522
524
|
- spec/punchblock/translator/asterisk/component/output_spec.rb
|
|
525
|
+
- spec/punchblock/translator/asterisk/component/record_spec.rb
|
|
523
526
|
- spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb
|
|
524
527
|
- spec/punchblock/translator/asterisk/component_spec.rb
|
|
525
528
|
- spec/punchblock/translator/asterisk_spec.rb
|