punchblock 1.2.0 → 1.3.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.
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