punchblock 0.7.1 → 0.7.2

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 (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