punchblock 0.7.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGELOG.md +12 -0
  2. data/lib/punchblock.rb +1 -0
  3. data/lib/punchblock/command/join.rb +6 -6
  4. data/lib/punchblock/command/unjoin.rb +6 -6
  5. data/lib/punchblock/command_node.rb +1 -0
  6. data/lib/punchblock/component/input.rb +24 -4
  7. data/lib/punchblock/component/output.rb +5 -1
  8. data/lib/punchblock/component/tropo/ask.rb +3 -1
  9. data/lib/punchblock/connection/xmpp.rb +28 -10
  10. data/lib/punchblock/event/joined.rb +6 -6
  11. data/lib/punchblock/event/unjoined.rb +6 -6
  12. data/lib/punchblock/media_container.rb +6 -5
  13. data/lib/punchblock/protocol_error.rb +5 -0
  14. data/lib/punchblock/rayo_node.rb +1 -1
  15. data/lib/punchblock/translator/asterisk.rb +3 -3
  16. data/lib/punchblock/translator/asterisk/call.rb +9 -3
  17. data/lib/punchblock/translator/asterisk/component.rb +35 -0
  18. data/lib/punchblock/translator/asterisk/component/asterisk.rb +1 -0
  19. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +14 -23
  20. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +5 -18
  21. data/lib/punchblock/translator/asterisk/component/asterisk/output.rb +94 -0
  22. data/lib/punchblock/version.rb +1 -1
  23. data/punchblock.gemspec +1 -0
  24. data/spec/punchblock/command/join_spec.rb +4 -4
  25. data/spec/punchblock/command/unjoin_spec.rb +4 -4
  26. data/spec/punchblock/component/input_spec.rb +28 -31
  27. data/spec/punchblock/component/output_spec.rb +23 -5
  28. data/spec/punchblock/component/tropo/ask_spec.rb +31 -34
  29. data/spec/punchblock/connection/xmpp_spec.rb +105 -3
  30. data/spec/punchblock/event/joined_spec.rb +4 -4
  31. data/spec/punchblock/event/unjoined_spec.rb +4 -4
  32. data/spec/punchblock/protocol_error_spec.rb +32 -1
  33. data/spec/punchblock/translator/asterisk/call_spec.rb +17 -3
  34. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +17 -0
  35. data/spec/punchblock/translator/asterisk/component/asterisk/output_spec.rb +489 -0
  36. data/spec/punchblock/translator/asterisk_spec.rb +14 -3
  37. metadata +53 -44
  38. data/assets/ozone/ask-1.0.xsd +0 -56
  39. data/assets/ozone/conference-1.0.xsd +0 -17
  40. data/assets/ozone/ozone-1.0.xsd +0 -127
  41. data/assets/ozone/say-1.0.xsd +0 -24
  42. 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
@@ -7,6 +7,7 @@ module Punchblock
7
7
 
8
8
  autoload :AGICommand
9
9
  autoload :AMIAction
10
+ autoload :Output
10
11
  end
11
12
  end
12
13
  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 initialize(component_node, call)
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' => @component_node.name, 'CommandID' => id do |response|
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
- set_node_response Ref.new :id => id
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
- @component_node, @translator = component_node, translator
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
@@ -1,3 +1,3 @@
1
1
  module Punchblock
2
- VERSION = "0.7.1"
2
+ VERSION = "0.7.2"
3
3
  end
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', :mixer_id => 'blah', :direction => :duplex, :media => :bridge }
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(:mixer_id) { should == 'blah' }
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-id="blah"
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(:mixer_id) { should == 'blah' }
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', :mixer_id => 'blah' }
12
+ subject { Unjoin.new :other_call_id => 'abc123', :mixer_name => 'blah' }
13
13
 
14
14
  its(:other_call_id) { should == 'abc123' }
15
- its(:mixer_id) { should == 'blah' }
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-id="blah" />
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(:mixer_id) { should == 'blah' }
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 grammar" do
86
- subject { Input::Grammar.new :value => '[5 DIGITS]' }
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 => grxml, :content_type => 'application/grammar+grxml' }
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
- let(:expected_message) { "<![CDATA[ #{grxml} ]]>" }
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
- subject { Output.new :ssml => '<output-as interpret-as="ordinal">100</output-as>', :voice => 'kate' }
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 == '<output-as interpret-as="ordinal">100</output-as>' }
86
+ its(:ssml) { should == ssml_doc }
71
87
 
72
88
  describe "comparison" do
73
- let(:output2) { Output.new :ssml => '<output-as interpret-as="ordinal">100</output-as>', :voice => 'kate' }
74
- let(:output3) { Output.new :ssml => '<output-as interpret-as="number">100</output-as>', :voice => 'kate' }
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 { should_not == output3 }
94
+ it { should == output3 }
95
+ it { should_not == output4 }
78
96
  end
79
97
  end
80
98