punchblock 0.7.1 → 0.7.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +12 -0
- data/lib/punchblock.rb +1 -0
- data/lib/punchblock/command/join.rb +6 -6
- data/lib/punchblock/command/unjoin.rb +6 -6
- data/lib/punchblock/command_node.rb +1 -0
- data/lib/punchblock/component/input.rb +24 -4
- data/lib/punchblock/component/output.rb +5 -1
- data/lib/punchblock/component/tropo/ask.rb +3 -1
- data/lib/punchblock/connection/xmpp.rb +28 -10
- data/lib/punchblock/event/joined.rb +6 -6
- data/lib/punchblock/event/unjoined.rb +6 -6
- data/lib/punchblock/media_container.rb +6 -5
- data/lib/punchblock/protocol_error.rb +5 -0
- data/lib/punchblock/rayo_node.rb +1 -1
- data/lib/punchblock/translator/asterisk.rb +3 -3
- data/lib/punchblock/translator/asterisk/call.rb +9 -3
- data/lib/punchblock/translator/asterisk/component.rb +35 -0
- data/lib/punchblock/translator/asterisk/component/asterisk.rb +1 -0
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +14 -23
- data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +5 -18
- data/lib/punchblock/translator/asterisk/component/asterisk/output.rb +94 -0
- data/lib/punchblock/version.rb +1 -1
- data/punchblock.gemspec +1 -0
- data/spec/punchblock/command/join_spec.rb +4 -4
- data/spec/punchblock/command/unjoin_spec.rb +4 -4
- data/spec/punchblock/component/input_spec.rb +28 -31
- data/spec/punchblock/component/output_spec.rb +23 -5
- data/spec/punchblock/component/tropo/ask_spec.rb +31 -34
- data/spec/punchblock/connection/xmpp_spec.rb +105 -3
- data/spec/punchblock/event/joined_spec.rb +4 -4
- data/spec/punchblock/event/unjoined_spec.rb +4 -4
- data/spec/punchblock/protocol_error_spec.rb +32 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +17 -3
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +17 -0
- data/spec/punchblock/translator/asterisk/component/asterisk/output_spec.rb +489 -0
- data/spec/punchblock/translator/asterisk_spec.rb +14 -3
- metadata +53 -44
- data/assets/ozone/ask-1.0.xsd +0 -56
- data/assets/ozone/conference-1.0.xsd +0 -17
- data/assets/ozone/ozone-1.0.xsd +0 -127
- data/assets/ozone/say-1.0.xsd +0 -24
- data/assets/ozone/transfer-1.0.xsd +0 -32
@@ -10,6 +10,41 @@ module Punchblock
|
|
10
10
|
include Celluloid
|
11
11
|
|
12
12
|
attr_reader :id
|
13
|
+
|
14
|
+
def initialize(component_node, call = nil)
|
15
|
+
@component_node, @call = component_node, call
|
16
|
+
@id = UUIDTools::UUID.random_create.to_s
|
17
|
+
setup
|
18
|
+
pb_logger.debug "Starting up..."
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_node_response(value)
|
25
|
+
pb_logger.debug "Setting response on component node to #{value}"
|
26
|
+
@component_node.response = value
|
27
|
+
end
|
28
|
+
|
29
|
+
def send_ref
|
30
|
+
set_node_response Ref.new :id => id
|
31
|
+
end
|
32
|
+
|
33
|
+
def with_error(name, text)
|
34
|
+
set_node_response ProtocolError.new(name, text)
|
35
|
+
end
|
36
|
+
|
37
|
+
def complete_event(reason)
|
38
|
+
Punchblock::Event::Complete.new.tap do |c|
|
39
|
+
c.reason = reason
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def send_event(event)
|
44
|
+
event.component_id = id
|
45
|
+
pb_logger.debug "Sending event #{event}"
|
46
|
+
@component_node.add_event event
|
47
|
+
end
|
13
48
|
end
|
14
49
|
end
|
15
50
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'uri'
|
2
|
+
require 'active_support/core_ext/string/filters'
|
2
3
|
|
3
4
|
module Punchblock
|
4
5
|
module Translator
|
@@ -8,11 +9,8 @@ module Punchblock
|
|
8
9
|
class AGICommand < Component
|
9
10
|
attr_reader :action
|
10
11
|
|
11
|
-
def
|
12
|
-
@component_node, @call = component_node, call
|
13
|
-
@id = UUIDTools::UUID.random_create.to_s
|
12
|
+
def setup
|
14
13
|
@action = create_action
|
15
|
-
pb_logger.debug "Starting up..."
|
16
14
|
end
|
17
15
|
|
18
16
|
def execute
|
@@ -40,42 +38,35 @@ module Punchblock
|
|
40
38
|
private
|
41
39
|
|
42
40
|
def create_action
|
43
|
-
RubyAMI::Action.new 'AGI', 'Channel' => @call.channel, 'Command' =>
|
41
|
+
RubyAMI::Action.new 'AGI', 'Channel' => @call.channel, 'Command' => agi_command, 'CommandID' => id do |response|
|
44
42
|
handle_response response
|
45
43
|
end
|
46
44
|
end
|
47
45
|
|
46
|
+
def agi_command
|
47
|
+
"#{@component_node.name} #{@component_node.params_array.map { |arg| quote_arg(arg) }.join(' ')}".squish
|
48
|
+
end
|
49
|
+
|
50
|
+
# Arguments surrounded by quotes; quotes backslash-escaped.
|
51
|
+
# See parse_args in asterisk/res/res_agi.c (Asterisk 1.4.21.1)
|
52
|
+
def quote_arg(arg)
|
53
|
+
'"' + arg.to_s.gsub(/["\\]/) { |m| "\\#{m}" } + '"'
|
54
|
+
end
|
55
|
+
|
48
56
|
def handle_response(response)
|
49
57
|
pb_logger.debug "Handling response: #{response.inspect}"
|
50
58
|
case response
|
51
59
|
when RubyAMI::Error
|
52
60
|
set_node_response false
|
53
61
|
when RubyAMI::Response
|
54
|
-
|
62
|
+
send_ref
|
55
63
|
end
|
56
64
|
end
|
57
65
|
|
58
|
-
def set_node_response(value)
|
59
|
-
pb_logger.debug "Setting response on component node to #{value}"
|
60
|
-
@component_node.response = value
|
61
|
-
end
|
62
|
-
|
63
66
|
def success_reason(event)
|
64
67
|
code, result, data = parse_agi_result event['Result']
|
65
68
|
Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new :code => code, :result => result, :data => data
|
66
69
|
end
|
67
|
-
|
68
|
-
def complete_event(reason)
|
69
|
-
Punchblock::Event::Complete.new.tap do |c|
|
70
|
-
c.reason = reason
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def send_event(event)
|
75
|
-
event.component_id = id
|
76
|
-
pb_logger.debug "Sending event #{event.inspect}"
|
77
|
-
@component_node.add_event event
|
78
|
-
end
|
79
70
|
end
|
80
71
|
end
|
81
72
|
end
|
@@ -7,10 +7,13 @@ module Punchblock
|
|
7
7
|
attr_reader :action
|
8
8
|
|
9
9
|
def initialize(component_node, translator)
|
10
|
-
|
10
|
+
super
|
11
|
+
@translator = translator
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup
|
11
15
|
@action = create_action
|
12
16
|
@id = @action.action_id
|
13
|
-
pb_logger.debug "Starting up..."
|
14
17
|
end
|
15
18
|
|
16
19
|
def execute
|
@@ -34,10 +37,6 @@ module Punchblock
|
|
34
37
|
@translator.send_ami_action! @action
|
35
38
|
end
|
36
39
|
|
37
|
-
def send_ref
|
38
|
-
@component_node.response = Ref.new :id => @action.action_id
|
39
|
-
end
|
40
|
-
|
41
40
|
def handle_response(response)
|
42
41
|
pb_logger.debug "Handling response #{response.inspect}"
|
43
42
|
case response
|
@@ -60,12 +59,6 @@ module Punchblock
|
|
60
59
|
Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new :message => headers.delete('Message'), :attributes => headers
|
61
60
|
end
|
62
61
|
|
63
|
-
def complete_event(reason)
|
64
|
-
Punchblock::Event::Complete.new.tap do |c|
|
65
|
-
c.reason = reason
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
62
|
def send_events
|
70
63
|
return unless @action.has_causal_events?
|
71
64
|
@action.events.each do |e|
|
@@ -82,12 +75,6 @@ module Punchblock
|
|
82
75
|
headers.delete 'ActionID'
|
83
76
|
Event::Asterisk::AMI::Event.new :name => ami_event.name, :attributes => headers
|
84
77
|
end
|
85
|
-
|
86
|
-
def send_event(event)
|
87
|
-
event.component_id = id
|
88
|
-
pb_logger.debug "Sending event #{event}"
|
89
|
-
@component_node.add_event event
|
90
|
-
end
|
91
78
|
end
|
92
79
|
end
|
93
80
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'active_support/core_ext/string/filters'
|
2
|
+
|
3
|
+
module Punchblock
|
4
|
+
module Translator
|
5
|
+
class Asterisk
|
6
|
+
module Component
|
7
|
+
module Asterisk
|
8
|
+
class Output < Component
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@media_engine = @call.translator.media_engine
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute
|
15
|
+
return with_error 'option error', 'An SSML document is required.' unless @component_node.ssml
|
16
|
+
|
17
|
+
return with_error 'option error', 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :speech
|
18
|
+
|
19
|
+
[:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
|
20
|
+
return with_error 'option error', "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
|
21
|
+
end
|
22
|
+
|
23
|
+
case @media_engine
|
24
|
+
when :asterisk, nil
|
25
|
+
return with_error 'option error', "A voice value is unsupported on Asterisk." if @component_node.voice
|
26
|
+
|
27
|
+
@execution_elements = @component_node.ssml.children.map do |node|
|
28
|
+
case node
|
29
|
+
when RubySpeech::SSML::Audio
|
30
|
+
lambda { current_actor.play_audio! node.src }
|
31
|
+
end
|
32
|
+
end.compact
|
33
|
+
|
34
|
+
@pending_actions = @execution_elements.count
|
35
|
+
|
36
|
+
send_ref
|
37
|
+
|
38
|
+
@interrupt_digits = '0123456789*#' if [:any, :dtmf].include? @component_node.interrupt_on
|
39
|
+
|
40
|
+
@execution_elements.each do |element|
|
41
|
+
element.call
|
42
|
+
wait :continue
|
43
|
+
process_playback_completion
|
44
|
+
end
|
45
|
+
when :unimrcp
|
46
|
+
doc = @component_node.ssml.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
|
47
|
+
send_ref
|
48
|
+
@call.send_agi_action! 'EXEC MRCPSynth', doc, mrcpsynth_options do |complete_event|
|
49
|
+
pb_logger.debug "MRCPSynth completed with #{complete_event}."
|
50
|
+
send_event complete_event(success_reason)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_playback_completion
|
56
|
+
@pending_actions -= 1
|
57
|
+
pb_logger.debug "Received action completion. Now waiting on #{@pending_actions} actions."
|
58
|
+
if @pending_actions < 1
|
59
|
+
pb_logger.debug "Sending complete event"
|
60
|
+
send_event complete_event(success_reason)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def continue(event = nil)
|
65
|
+
signal :continue, event
|
66
|
+
end
|
67
|
+
|
68
|
+
def play_audio(path)
|
69
|
+
pb_logger.debug "Playing an audio file (#{path}) via STREAM FILE"
|
70
|
+
op = current_actor
|
71
|
+
@call.send_agi_action! 'STREAM FILE', path, @interrupt_digits do |complete_event|
|
72
|
+
pb_logger.debug "STREAM FILE completed with #{complete_event}. Signalling to continue execution."
|
73
|
+
op.continue! complete_event
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def mrcpsynth_options
|
80
|
+
[].tap do |opts|
|
81
|
+
opts << 'i=any' if [:any, :dtmf].include? @component_node.interrupt_on
|
82
|
+
opts << "v=#{@component_node.voice}" if @component_node.voice
|
83
|
+
end.join '&'
|
84
|
+
end
|
85
|
+
|
86
|
+
def success_reason
|
87
|
+
Punchblock::Component::Output::Complete::Success.new
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/punchblock/version.rb
CHANGED
data/punchblock.gemspec
CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |s|
|
|
30
30
|
s.add_runtime_dependency %q<has-guarded-handlers>, [">= 0.1.0"]
|
31
31
|
s.add_runtime_dependency %q<celluloid>, [">= 0.6.0"]
|
32
32
|
s.add_runtime_dependency %q<ruby_ami>, [">= 0.1.3"]
|
33
|
+
s.add_runtime_dependency %q<ruby_speech>, [">= 0.3.4"]
|
33
34
|
|
34
35
|
s.add_development_dependency %q<bundler>, ["~> 1.0.0"]
|
35
36
|
s.add_development_dependency %q<rspec>, [">= 2.5.0"]
|
@@ -9,10 +9,10 @@ module Punchblock
|
|
9
9
|
end
|
10
10
|
|
11
11
|
describe "when setting options in initializer" do
|
12
|
-
subject { Join.new :other_call_id => 'abc123', :
|
12
|
+
subject { Join.new :other_call_id => 'abc123', :mixer_name => 'blah', :direction => :duplex, :media => :bridge }
|
13
13
|
|
14
14
|
its(:other_call_id) { should == 'abc123' }
|
15
|
-
its(:
|
15
|
+
its(:mixer_name) { should == 'blah' }
|
16
16
|
its(:direction) { should == :duplex }
|
17
17
|
its(:media) { should == :bridge }
|
18
18
|
end
|
@@ -22,7 +22,7 @@ module Punchblock
|
|
22
22
|
<<-MESSAGE
|
23
23
|
<join xmlns="urn:xmpp:rayo:1"
|
24
24
|
call-id="abc123"
|
25
|
-
mixer-
|
25
|
+
mixer-name="blah"
|
26
26
|
direction="duplex"
|
27
27
|
media="bridge" />
|
28
28
|
MESSAGE
|
@@ -33,7 +33,7 @@ module Punchblock
|
|
33
33
|
it { should be_instance_of Join }
|
34
34
|
|
35
35
|
its(:other_call_id) { should == 'abc123' }
|
36
|
-
its(:
|
36
|
+
its(:mixer_name) { should == 'blah' }
|
37
37
|
its(:direction) { should == :duplex }
|
38
38
|
its(:media) { should == :bridge }
|
39
39
|
end
|
@@ -9,10 +9,10 @@ module Punchblock
|
|
9
9
|
end
|
10
10
|
|
11
11
|
describe "when setting options in initializer" do
|
12
|
-
subject { Unjoin.new :other_call_id => 'abc123', :
|
12
|
+
subject { Unjoin.new :other_call_id => 'abc123', :mixer_name => 'blah' }
|
13
13
|
|
14
14
|
its(:other_call_id) { should == 'abc123' }
|
15
|
-
its(:
|
15
|
+
its(:mixer_name) { should == 'blah' }
|
16
16
|
end
|
17
17
|
|
18
18
|
describe "from a stanza" do
|
@@ -20,7 +20,7 @@ module Punchblock
|
|
20
20
|
<<-MESSAGE
|
21
21
|
<unjoin xmlns="urn:xmpp:rayo:1"
|
22
22
|
call-id="abc123"
|
23
|
-
mixer-
|
23
|
+
mixer-name="blah" />
|
24
24
|
MESSAGE
|
25
25
|
end
|
26
26
|
|
@@ -29,7 +29,7 @@ module Punchblock
|
|
29
29
|
it { should be_instance_of Unjoin }
|
30
30
|
|
31
31
|
its(:other_call_id) { should == 'abc123' }
|
32
|
-
its(:
|
32
|
+
its(:mixer_name) { should == 'blah' }
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
@@ -81,9 +81,19 @@ module Punchblock
|
|
81
81
|
its(:min_confidence) { should == 0.5 }
|
82
82
|
end
|
83
83
|
|
84
|
+
def grxml_doc(mode = :dtmf)
|
85
|
+
RubySpeech::GRXML.draw :mode => mode.to_s, :root => 'digits' do
|
86
|
+
rule id: 'digits' do
|
87
|
+
one_of do
|
88
|
+
0.upto(1) { |d| item { d.to_s } }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
84
94
|
describe Input::Grammar do
|
85
|
-
describe "when not passing a
|
86
|
-
subject { Input::Grammar.new :value =>
|
95
|
+
describe "when not passing a content type" do
|
96
|
+
subject { Input::Grammar.new :value => grxml_doc }
|
87
97
|
its(:content_type) { should == 'application/grammar+grxml' }
|
88
98
|
end
|
89
99
|
|
@@ -98,40 +108,27 @@ module Punchblock
|
|
98
108
|
end
|
99
109
|
|
100
110
|
describe 'with a GRXML grammar' do
|
101
|
-
subject { Input::Grammar.new :value =>
|
102
|
-
|
103
|
-
let :grxml do
|
104
|
-
<<-GRXML
|
105
|
-
<grammar xmlns="http://www.w3.org/2001/06/grammar" root="MAINRULE">
|
106
|
-
<rule id="MAINRULE">
|
107
|
-
<one-of>
|
108
|
-
<item>
|
109
|
-
<item repeat="0-1"> need a</item>
|
110
|
-
<item repeat="0-1"> i need a</item>
|
111
|
-
<one-of>
|
112
|
-
<item> clue </item>
|
113
|
-
</one-of>
|
114
|
-
<tag> out.concept = "clue";</tag>
|
115
|
-
</item>
|
116
|
-
<item>
|
117
|
-
<item repeat="0-1"> have an</item>
|
118
|
-
<item repeat="0-1"> i have an</item>
|
119
|
-
<one-of>
|
120
|
-
<item> answer </item>
|
121
|
-
</one-of>
|
122
|
-
<tag> out.concept = "answer";</tag>
|
123
|
-
</item>
|
124
|
-
</one-of>
|
125
|
-
</rule>
|
126
|
-
</grammar>
|
127
|
-
GRXML
|
128
|
-
end
|
111
|
+
subject { Input::Grammar.new :value => grxml_doc, :content_type => 'application/grammar+grxml' }
|
129
112
|
|
130
|
-
|
113
|
+
its(:content_type) { should == 'application/grammar+grxml' }
|
114
|
+
|
115
|
+
let(:expected_message) { "<![CDATA[ #{grxml_doc} ]]>" }
|
131
116
|
|
132
117
|
it "should wrap GRXML in CDATA" do
|
133
118
|
subject.child.to_xml.should == expected_message.strip
|
134
119
|
end
|
120
|
+
|
121
|
+
its(:value) { should == grxml_doc }
|
122
|
+
|
123
|
+
describe "comparison" do
|
124
|
+
let(:grammar2) { Input::Grammar.new :value => '<grammar xmlns="http://www.w3.org/2001/06/grammar" version="1.0" xml:lang="en-US" mode="dtmf" root="digits"><rule id="digits"><one-of><item>0</item><item>1</item></one-of></rule></grammar>' }
|
125
|
+
let(:grammar3) { Input::Grammar.new :value => grxml_doc }
|
126
|
+
let(:grammar4) { Input::Grammar.new :value => grxml_doc(:speech) }
|
127
|
+
|
128
|
+
it { should == grammar2 }
|
129
|
+
it { should == grammar3 }
|
130
|
+
it { should_not == grammar4 }
|
131
|
+
end
|
135
132
|
end
|
136
133
|
end
|
137
134
|
|
@@ -7,6 +7,16 @@ module Punchblock
|
|
7
7
|
RayoNode.class_from_registration(:output, 'urn:xmpp:rayo:output:1').should == Output
|
8
8
|
end
|
9
9
|
|
10
|
+
describe 'default values' do
|
11
|
+
its(:interrupt_on) { should be nil }
|
12
|
+
its(:start_offset) { should be nil }
|
13
|
+
its(:start_paused) { should be false }
|
14
|
+
its(:repeat_interval) { should be nil }
|
15
|
+
its(:repeat_times) { should be nil }
|
16
|
+
its(:max_time) { should be nil }
|
17
|
+
its(:voice) { should be nil }
|
18
|
+
end
|
19
|
+
|
10
20
|
describe "when setting options in initializer" do
|
11
21
|
subject do
|
12
22
|
Output.new :interrupt_on => :speech,
|
@@ -63,18 +73,26 @@ module Punchblock
|
|
63
73
|
end
|
64
74
|
|
65
75
|
describe "for SSML" do
|
66
|
-
|
76
|
+
def ssml_doc(mode = :ordinal)
|
77
|
+
RubySpeech::SSML.draw do
|
78
|
+
say_as(:interpret_as => mode) { 100 }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
subject { Output.new :ssml => ssml_doc, :voice => 'kate' }
|
67
83
|
|
68
84
|
its(:voice) { should == 'kate' }
|
69
85
|
|
70
|
-
its(:ssml) { should ==
|
86
|
+
its(:ssml) { should == ssml_doc }
|
71
87
|
|
72
88
|
describe "comparison" do
|
73
|
-
let(:output2) { Output.new :ssml => '<
|
74
|
-
let(:output3) { Output.new :ssml =>
|
89
|
+
let(:output2) { Output.new :ssml => '<speak xmlns="http://www.w3.org/2001/10/synthesis" version="1.0" xml:lang="en-US"><say-as interpret-as="ordinal"/></speak>', :voice => 'kate' }
|
90
|
+
let(:output3) { Output.new :ssml => ssml_doc, :voice => 'kate' }
|
91
|
+
let(:output4) { Output.new :ssml => ssml_doc(:normal), :voice => 'kate' }
|
75
92
|
|
76
93
|
it { should == output2 }
|
77
|
-
it {
|
94
|
+
it { should == output3 }
|
95
|
+
it { should_not == output4 }
|
78
96
|
end
|
79
97
|
end
|
80
98
|
|