punchblock 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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,13 +16,15 @@ module Punchblock
16
16
  OptionError = Class.new Punchblock::Error
17
17
 
18
18
  include Celluloid
19
+ include DeadActorSafety
19
20
 
20
- attr_reader :id, :call
21
+ attr_reader :id, :call, :call_id
21
22
  attr_accessor :internal
22
23
 
23
24
  def initialize(component_node, call = nil)
24
25
  @component_node, @call = component_node, call
25
- @id = UUIDTools::UUID.random_create.to_s
26
+ @call_id = safe_from_dead_actors { call.id } if call
27
+ @id = Punchblock.new_uuid
26
28
  @complete = false
27
29
  setup
28
30
  pb_logger.debug "Starting up..."
@@ -53,16 +55,12 @@ module Punchblock
53
55
  if internal
54
56
  @component_node.add_event event
55
57
  else
56
- translator.handle_pb_event! event
58
+ safe_from_dead_actors { translator.handle_pb_event event }
57
59
  end
58
60
  end
59
61
 
60
62
  def logger_id
61
- "#{self.class}: #{call ? "Call ID: #{call.id}, Component ID: #{id}" : id}"
62
- end
63
-
64
- def call_id
65
- call.id if call
63
+ "#{self.class}: #{call_id ? "Call ID: #{call_id}, Component ID: #{id}" : id}"
66
64
  end
67
65
 
68
66
  def call_ended
@@ -1,6 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'uri'
4
3
  require 'active_support/core_ext/string/filters'
5
4
 
6
5
  module Punchblock
@@ -16,7 +15,8 @@ module Punchblock
16
15
  end
17
16
 
18
17
  def execute
19
- @call.send_ami_action! @action
18
+ send_ref
19
+ @call.send_ami_action @action
20
20
  end
21
21
 
22
22
  def handle_ami_event(event)
@@ -29,19 +29,22 @@ module Punchblock
29
29
  end
30
30
  end
31
31
 
32
- def parse_agi_result(result)
33
- match = URI::Parser.new.unescape(result).chomp.match(/^(\d{3}) result=(-?\d*) ?(\(?.*\)?)?$/)
34
- if match
35
- data = match[3] ? match[3].gsub(/(^\()|(\)$)/, '') : nil
36
- [match[1].to_i, match[2].to_i, data]
32
+ def handle_response(response)
33
+ pb_logger.debug "Handling response: #{response.inspect}"
34
+ case response
35
+ when RubyAMI::Error
36
+ set_node_response false
37
+ when RubyAMI::Response
38
+ send_ref
37
39
  end
38
40
  end
39
41
 
40
42
  private
41
43
 
42
44
  def create_action
45
+ command = current_actor
43
46
  RubyAMI::Action.new 'AGI', 'Channel' => @call.channel, 'Command' => agi_command, 'CommandID' => id do |response|
44
- handle_response response
47
+ command.handle_response response
45
48
  end
46
49
  end
47
50
 
@@ -55,19 +58,9 @@ module Punchblock
55
58
  '"' + arg.to_s.gsub(/["\\]/) { |m| "\\#{m}" } + '"'
56
59
  end
57
60
 
58
- def handle_response(response)
59
- pb_logger.debug "Handling response: #{response.inspect}"
60
- case response
61
- when RubyAMI::Error
62
- set_node_response false
63
- when RubyAMI::Response
64
- send_ref
65
- end
66
- end
67
-
68
61
  def success_reason(event)
69
- code, result, data = parse_agi_result event['Result']
70
- Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new :code => code, :result => result, :data => data
62
+ parser = RubyAMI::AGIResultParser.new event['Result']
63
+ Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new :code => parser.code, :result => parser.result, :data => parser.data
71
64
  end
72
65
  end
73
66
  end
@@ -48,7 +48,7 @@ module Punchblock
48
48
  end
49
49
 
50
50
  def send_action
51
- @translator.send_ami_action! @action
51
+ @translator.send_ami_action @action
52
52
  end
53
53
 
54
54
  def error_reason(response)
@@ -13,7 +13,7 @@ module Punchblock
13
13
  end
14
14
 
15
15
  def execute
16
- @call.answer_if_not_answered
16
+ @call.send_progress
17
17
  initial_timeout = @component_node.initial_timeout || -1
18
18
  @inter_digit_timeout = @component_node.inter_digit_timeout || -1
19
19
 
@@ -33,9 +33,7 @@ module Punchblock
33
33
 
34
34
  component = current_actor
35
35
 
36
- @active = true
37
-
38
- call.register_handler :ami, :name => 'DTMF' do |event|
36
+ @dtmf_handler_id = call.register_handler :ami, :name => 'DTMF' do |event|
39
37
  component.process_dtmf! event['Digit'] if event['End'] == 'Yes'
40
38
  end
41
39
  rescue OptionError => e
@@ -43,7 +41,6 @@ module Punchblock
43
41
  end
44
42
 
45
43
  def process_dtmf(digit)
46
- return unless @active
47
44
  pb_logger.trace "Processing incoming DTMF digit #{digit}"
48
45
  buffer << digit
49
46
  cancel_initial_timer
@@ -113,7 +110,7 @@ module Punchblock
113
110
  end
114
111
 
115
112
  def complete(reason)
116
- @active = false
113
+ call.unregister_handler :ami, @dtmf_handler_id if instance_variable_defined?(:@dtmf_handler_id)
117
114
  cancel_initial_timer
118
115
  cancel_inter_digit_timer
119
116
  send_complete_event reason
@@ -16,8 +16,6 @@ module Punchblock
16
16
  end
17
17
 
18
18
  def execute
19
- @call.answer_if_not_answered
20
-
21
19
  raise OptionError, 'An SSML document is required.' unless @component_node.ssml
22
20
  raise OptionError, 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :speech
23
21
 
@@ -25,40 +23,43 @@ module Punchblock
25
23
  raise OptionError, "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
26
24
  end
27
25
 
26
+ early = !@call.answered?
27
+
28
+ output_component = current_actor
29
+
28
30
  case @media_engine
29
31
  when :asterisk, nil
30
32
  raise OptionError, "A voice value is unsupported on Asterisk." if @component_node.voice
33
+ raise OptionError, 'Interrupt digits are not allowed with early media.' if early && @component_node.interrupt_on
34
+
35
+ case @component_node.interrupt_on
36
+ when :any, :dtmf
37
+ interrupt = true
38
+ end
31
39
 
32
- @execution_elements = collect_executable_elements
33
- @pending_actions = @execution_elements.count
40
+ path = filenames.join '&'
34
41
 
35
42
  send_ref
36
43
 
37
- @interrupt_digits = if [:any, :dtmf].include? @component_node.interrupt_on
38
- '0123456789*#'
39
- else
40
- nil
41
- end
44
+ @call.send_progress if early
42
45
 
43
- @execution_elements.each do |element|
44
- element.call
45
- wait :continue
46
- process_playback_completion
46
+ if interrupt
47
+ call.register_handler :ami, :name => 'DTMF' do |event|
48
+ output_component.stop_by_redirect Punchblock::Component::Output::Complete::Success.new if event['End'] == 'Yes'
49
+ end
47
50
  end
51
+
52
+ opts = early ? "#{path},noanswer" : path
53
+ playback opts
48
54
  when :unimrcp
49
55
  send_ref
50
- output_component = current_actor
51
56
  @call.send_agi_action! 'EXEC MRCPSynth', escaped_doc, mrcpsynth_options do |complete_event|
52
57
  pb_logger.debug "MRCPSynth completed with #{complete_event}."
53
58
  output_component.send_complete_event! success_reason
54
59
  end
55
60
  when :swift
56
- doc = escaped_doc
57
- doc << "|1|1" if [:any, :dtmf].include? @component_node.interrupt_on
58
- doc.insert 0, "#{@component_node.voice}^" if @component_node.voice
59
61
  send_ref
60
- output_component = current_actor
61
- @call.send_agi_action! 'EXEC Swift', doc do |complete_event|
62
+ @call.send_agi_action! 'EXEC Swift', swift_doc do |complete_event|
62
63
  pb_logger.debug "Swift completed with #{complete_event}."
63
64
  output_component.send_complete_event! success_reason
64
65
  end
@@ -69,46 +70,33 @@ module Punchblock
69
70
  with_error 'option error', e.message
70
71
  end
71
72
 
72
- def collect_executable_elements
73
- @component_node.ssml.children.map do |node|
73
+ private
74
+
75
+ def filenames
76
+ @filenames ||= @component_node.ssml.children.map do |node|
74
77
  case node
75
78
  when RubySpeech::SSML::Audio
76
- lambda { current_actor.play_audio! node.src }
79
+ node.src
77
80
  when String
78
- raise UnrenderableDocError, 'The provided document could not be rendered.' if node.include?(' ')
79
- lambda { current_actor.play_audio! node }
81
+ raise if node.include?(' ')
82
+ node
80
83
  else
81
- raise UnrenderableDocError, 'The provided document could not be rendered.'
84
+ raise
82
85
  end
83
86
  end.compact
84
87
  rescue
85
88
  raise UnrenderableDocError, 'The provided document could not be rendered.'
86
89
  end
87
90
 
88
- def process_playback_completion
89
- @pending_actions -= 1
90
- pb_logger.debug "Received action completion. Now waiting on #{@pending_actions} actions."
91
- if @pending_actions < 1
92
- pb_logger.debug "Sending complete event"
93
- send_complete_event success_reason
94
- end
95
- end
96
-
97
- def continue(event = nil)
98
- signal :continue, event
99
- end
100
-
101
- def play_audio(path)
102
- pb_logger.debug "Playing an audio file (#{path}) via STREAM FILE"
91
+ def playback(path)
92
+ pb_logger.debug "Playing an audio file (#{path}) via Playback"
103
93
  op = current_actor
104
- @call.send_agi_action! 'STREAM FILE', path, @interrupt_digits do |complete_event|
105
- pb_logger.debug "STREAM FILE completed with #{complete_event}. Signalling to continue execution."
106
- op.continue! complete_event
94
+ @call.send_agi_action! 'EXEC Playback', path do |complete_event|
95
+ pb_logger.debug "File playback completed with #{complete_event}. Sending complete event"
96
+ op.send_complete_event! success_reason
107
97
  end
108
98
  end
109
99
 
110
- private
111
-
112
100
  def escaped_doc
113
101
  @component_node.ssml.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
114
102
  end
@@ -120,6 +108,13 @@ module Punchblock
120
108
  end.join '&'
121
109
  end
122
110
 
111
+ def swift_doc
112
+ doc = escaped_doc
113
+ doc << "|1|1" if [:any, :dtmf].include? @component_node.interrupt_on
114
+ doc.insert 0, "#{@component_node.voice}^" if @component_node.voice
115
+ doc
116
+ end
117
+
123
118
  def success_reason
124
119
  Punchblock::Component::Output::Complete::Success.new
125
120
  end
@@ -14,6 +14,7 @@ module Punchblock
14
14
  def execute
15
15
  max_duration = @component_node.max_duration || -1
16
16
 
17
+ raise OptionError, 'Record cannot be used on a call that is not answered.' unless @call.answered?
17
18
  raise OptionError, 'A start-paused value of true is unsupported.' if @component_node.start_paused
18
19
  raise OptionError, 'An initial-timeout value is unsupported.' if @component_node.initial_timeout && @component_node.initial_timeout != -1
19
20
  raise OptionError, 'A final-timeout value is unsupported.' if @component_node.final_timeout && @component_node.final_timeout != -1
@@ -21,7 +22,6 @@ module Punchblock
21
22
 
22
23
  @format = @component_node.format || 'wav'
23
24
 
24
- @call.answer_if_not_answered
25
25
 
26
26
  component = current_actor
27
27
  call.register_handler :ami, :name => 'MonitorStop' do |event|
@@ -9,12 +9,15 @@ module Punchblock
9
9
  module StopByRedirect
10
10
  def execute_command(command)
11
11
  return super unless command.is_a?(Punchblock::Component::Stop)
12
+ stop_by_redirect Punchblock::Event::Complete::Stop.new
13
+ command.response = true
14
+ end
12
15
 
16
+ def stop_by_redirect(complete_reason)
13
17
  component_actor = current_actor
14
18
  call.register_handler :ami, lambda { |e| e['SubEvent'] == 'Start' }, :name => 'AsyncAGI' do |event|
15
- component_actor.send_complete_event! Punchblock::Event::Complete::Stop.new
19
+ component_actor.send_complete_event! complete_reason
16
20
  end
17
- command.response = true
18
21
  call.redirect_back!
19
22
  end
20
23
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Punchblock
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
data/punchblock.gemspec CHANGED
@@ -29,13 +29,13 @@ Gem::Specification.new do |s|
29
29
  s.add_runtime_dependency %q<future-resource>, ["~> 1.0"]
30
30
  s.add_runtime_dependency %q<has-guarded-handlers>, ["~> 1.0"]
31
31
  s.add_runtime_dependency %q<celluloid>, [">= 0.10.0"]
32
- s.add_runtime_dependency %q<ruby_ami>, ["~> 1.0"]
32
+ s.add_runtime_dependency %q<ruby_ami>, ["~> 1.2", ">= 1.2.1"]
33
33
  s.add_runtime_dependency %q<ruby_speech>, ["~> 1.0"]
34
34
 
35
- s.add_development_dependency %q<bundler>, [">= 1.0.0"]
36
- s.add_development_dependency %q<rspec>, ["~> 2.7.0"]
37
- s.add_development_dependency %q<ci_reporter>, [">= 1.6.3"]
38
- s.add_development_dependency %q<yard>, [">= 0.6.0"]
35
+ s.add_development_dependency %q<bundler>, ["~> 1.0"]
36
+ s.add_development_dependency %q<rspec>, ["~> 2.7"]
37
+ s.add_development_dependency %q<ci_reporter>, ["~> 1.6"]
38
+ s.add_development_dependency %q<yard>, ["~> 0.6"]
39
39
  s.add_development_dependency %q<rake>, [">= 0"]
40
40
  s.add_development_dependency %q<mocha>, [">= 0"]
41
41
  s.add_development_dependency %q<i18n>, [">= 0"]
@@ -37,7 +37,7 @@ module Punchblock
37
37
  end
38
38
 
39
39
  describe "with the reason" do
40
- [:decline, :busy, :error].each do |reason|
40
+ [nil, :decline, :busy, :error].each do |reason|
41
41
  describe reason do
42
42
  subject { Reject.new :reason => reason }
43
43
 
@@ -45,6 +45,12 @@ module Punchblock
45
45
  end
46
46
  end
47
47
 
48
+ describe "no reason" do
49
+ subject { Reject.new }
50
+
51
+ its(:reason) { should be_nil }
52
+ end
53
+
48
54
  describe "blahblahblah" do
49
55
  it "should raise an error" do
50
56
  expect { Reject.new(:reason => :blahblahblah) }.to raise_error ArgumentError
@@ -5,6 +5,11 @@ require 'spec_helper'
5
5
  module Punchblock
6
6
  module Command
7
7
  describe CommandNode do
8
+ let(:args) { [] }
9
+ subject do
10
+ Class.new(described_class) { register 'foo'}.new(*args)
11
+ end
12
+
8
13
  its(:state_name) { should be == :new }
9
14
 
10
15
  describe "#new" do
@@ -13,8 +18,6 @@ module Punchblock
13
18
  let(:component_id) { 'abc123' }
14
19
  let(:args) { [{:target_call_id => call_id, :component_id => component_id}] }
15
20
 
16
- subject { CommandNode.new(*args) }
17
-
18
21
  its(:target_call_id) { should be == call_id }
19
22
  its(:component_id) { should be == component_id }
20
23
  end
@@ -5,6 +5,10 @@ require 'spec_helper'
5
5
  module Punchblock
6
6
  module Component
7
7
  describe ComponentNode do
8
+ subject do
9
+ Class.new(described_class) { register 'foo'}.new
10
+ end
11
+
8
12
  it { should be_new }
9
13
 
10
14
  describe "#add_event" do
@@ -230,7 +230,7 @@ module Punchblock
230
230
  describe '#seek_action' do
231
231
  subject { command.seek_action seek_options }
232
232
 
233
- its(:to_xml) { should be == '<seek xmlns="urn:xmpp:rayo:output:1" direction="forward" amount="1500"/>' }
233
+ its(:to_xml) { should be == Nokogiri::XML('<seek xmlns="urn:xmpp:rayo:output:1" direction="forward" amount="1500"/>').root.to_xml }
234
234
  its(:component_id) { should be == 'abc123' }
235
235
  its(:target_call_id) { should be == '123abc' }
236
236
  end
@@ -154,6 +154,36 @@ module Punchblock
154
154
  end
155
155
  end
156
156
 
157
+ context "direct recording accessors" do
158
+ let :stanza do
159
+ <<-MESSAGE
160
+ <complete xmlns='urn:xmpp:rayo:ext:1'>
161
+ <success xmlns='urn:xmpp:rayo:record:complete:1'/>
162
+ <recording xmlns='urn:xmpp:rayo:record:complete:1' uri="file:/tmp/rayo7451601434771683422.mp3" duration="34000" size="23450"/>
163
+ </complete>
164
+ MESSAGE
165
+ end
166
+ let(:event) { RayoNode.import(parse_stanza(stanza).root) }
167
+
168
+ before do
169
+ subject.request!
170
+ subject.execute!
171
+ subject.add_event event
172
+ end
173
+
174
+ describe "#recording" do
175
+ it "should be a Punchblock::Component::Record::Recording" do
176
+ subject.recording.should be_a Punchblock::Component::Record::Recording
177
+ end
178
+ end
179
+
180
+ describe "#recording_uri" do
181
+ it "should be the recording URI set earlier" do
182
+ subject.recording_uri.should be == "file:/tmp/rayo7451601434771683422.mp3"
183
+ end
184
+ end
185
+ end
186
+
157
187
  describe '#stop_action' do
158
188
  subject { command.stop_action }
159
189