punchblock 1.9.4 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +1 -2
  4. data/CHANGELOG.md +17 -0
  5. data/Gemfile +1 -0
  6. data/Guardfile +4 -0
  7. data/README.markdown +6 -0
  8. data/Rakefile +16 -0
  9. data/benchmarks/ami_event_name_comparison.rb +14 -0
  10. data/benchmarks/channel.rb +27 -0
  11. data/lib/punchblock/client.rb +2 -6
  12. data/lib/punchblock/command/accept.rb +3 -24
  13. data/lib/punchblock/command/answer.rb +3 -24
  14. data/lib/punchblock/command/dial.rb +24 -76
  15. data/lib/punchblock/command/hangup.rb +3 -19
  16. data/lib/punchblock/command/join.rb +21 -70
  17. data/lib/punchblock/command/mute.rb +3 -3
  18. data/lib/punchblock/command/redirect.rb +6 -39
  19. data/lib/punchblock/command/reject.rb +14 -54
  20. data/lib/punchblock/command/unjoin.rb +8 -40
  21. data/lib/punchblock/command/unmute.rb +3 -3
  22. data/lib/punchblock/command_node.rb +0 -17
  23. data/lib/punchblock/component/asterisk/agi/command.rb +20 -127
  24. data/lib/punchblock/component/asterisk/ami/action.rb +30 -117
  25. data/lib/punchblock/component/component_node.rb +1 -1
  26. data/lib/punchblock/component/input.rb +89 -268
  27. data/lib/punchblock/component/output.rb +106 -154
  28. data/lib/punchblock/component/prompt.rb +51 -0
  29. data/lib/punchblock/component/record.rb +41 -130
  30. data/lib/punchblock/component.rb +1 -0
  31. data/lib/punchblock/connection/asterisk.rb +31 -4
  32. data/lib/punchblock/connection/xmpp.rb +6 -14
  33. data/lib/punchblock/core_ext/blather/stanza.rb +1 -1
  34. data/lib/punchblock/event/active_speaker.rb +2 -10
  35. data/lib/punchblock/event/answered.rb +3 -3
  36. data/lib/punchblock/event/asterisk/ami/event.rb +15 -47
  37. data/lib/punchblock/event/complete.rb +26 -48
  38. data/lib/punchblock/event/dtmf.rb +3 -13
  39. data/lib/punchblock/event/end.rb +10 -11
  40. data/lib/punchblock/event/joined.rb +5 -25
  41. data/lib/punchblock/event/offer.rb +4 -25
  42. data/lib/punchblock/event/ringing.rb +3 -3
  43. data/lib/punchblock/event/unjoined.rb +5 -25
  44. data/lib/punchblock/event.rb +0 -10
  45. data/lib/punchblock/has_headers.rb +20 -26
  46. data/lib/punchblock/rayo_node.rb +46 -23
  47. data/lib/punchblock/ref.rb +39 -18
  48. data/lib/punchblock/translator/asterisk/agi_app.rb +15 -0
  49. data/lib/punchblock/translator/asterisk/agi_command.rb +3 -1
  50. data/lib/punchblock/translator/asterisk/ami_error_converter.rb +20 -0
  51. data/lib/punchblock/translator/asterisk/call.rb +60 -39
  52. data/lib/punchblock/translator/asterisk/channel.rb +41 -0
  53. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +4 -1
  54. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +4 -4
  55. data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +62 -0
  56. data/lib/punchblock/translator/asterisk/component/input.rb +1 -0
  57. data/lib/punchblock/translator/asterisk/component/mrcp_native_prompt.rb +56 -0
  58. data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +53 -0
  59. data/lib/punchblock/translator/asterisk/component/mrcp_recog_prompt.rb +99 -0
  60. data/lib/punchblock/translator/asterisk/component/output.rb +30 -22
  61. data/lib/punchblock/translator/asterisk/component/record.rb +8 -6
  62. data/lib/punchblock/translator/asterisk/component.rb +6 -5
  63. data/lib/punchblock/translator/asterisk/unimrcp_app.rb +26 -0
  64. data/lib/punchblock/translator/asterisk.rb +24 -28
  65. data/lib/punchblock/translator/dtmf_recognizer.rb +39 -20
  66. data/lib/punchblock/translator/freeswitch/call.rb +15 -14
  67. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +5 -4
  68. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
  69. data/lib/punchblock/translator/freeswitch/component/input.rb +5 -0
  70. data/lib/punchblock/translator/freeswitch/component/output.rb +2 -2
  71. data/lib/punchblock/translator/freeswitch/component/record.rb +19 -13
  72. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +2 -2
  73. data/lib/punchblock/translator/freeswitch/component.rb +2 -5
  74. data/lib/punchblock/translator/freeswitch.rb +2 -2
  75. data/lib/punchblock/translator/input_component.rb +33 -13
  76. data/lib/punchblock/uri_list.rb +21 -0
  77. data/lib/punchblock/version.rb +1 -1
  78. data/lib/punchblock.rb +4 -3
  79. data/punchblock.gemspec +7 -3
  80. data/spec/punchblock/client/component_registry_spec.rb +1 -1
  81. data/spec/punchblock/client_spec.rb +10 -26
  82. data/spec/punchblock/command/accept_spec.rb +41 -7
  83. data/spec/punchblock/command/answer_spec.rb +51 -7
  84. data/spec/punchblock/command/dial_spec.rb +56 -14
  85. data/spec/punchblock/command/hangup_spec.rb +41 -7
  86. data/spec/punchblock/command/join_spec.rb +53 -11
  87. data/spec/punchblock/command/mute_spec.rb +19 -4
  88. data/spec/punchblock/command/redirect_spec.rb +40 -10
  89. data/spec/punchblock/command/reject_spec.rb +43 -11
  90. data/spec/punchblock/command/unjoin_spec.rb +40 -9
  91. data/spec/punchblock/command/unmute_spec.rb +19 -4
  92. data/spec/punchblock/command_node_spec.rb +0 -4
  93. data/spec/punchblock/component/asterisk/agi/command_spec.rb +16 -39
  94. data/spec/punchblock/component/asterisk/ami/action_spec.rb +50 -53
  95. data/spec/punchblock/component/component_node_spec.rb +3 -5
  96. data/spec/punchblock/component/input_spec.rb +194 -61
  97. data/spec/punchblock/component/output_spec.rb +194 -62
  98. data/spec/punchblock/component/prompt_spec.rb +132 -0
  99. data/spec/punchblock/component/record_spec.rb +70 -32
  100. data/spec/punchblock/connection/asterisk_spec.rb +17 -3
  101. data/spec/punchblock/connection/freeswitch_spec.rb +4 -4
  102. data/spec/punchblock/connection/xmpp_spec.rb +20 -38
  103. data/spec/punchblock/event/answered_spec.rb +12 -10
  104. data/spec/punchblock/event/asterisk/ami/event_spec.rb +27 -22
  105. data/spec/punchblock/event/complete_spec.rb +15 -19
  106. data/spec/punchblock/event/dtmf_spec.rb +5 -6
  107. data/spec/punchblock/event/end_spec.rb +20 -10
  108. data/spec/punchblock/event/joined_spec.rb +8 -7
  109. data/spec/punchblock/event/offer_spec.rb +41 -12
  110. data/spec/punchblock/event/ringing_spec.rb +12 -10
  111. data/spec/punchblock/event/started_speaking_spec.rb +5 -6
  112. data/spec/punchblock/event/stopped_speaking_spec.rb +5 -6
  113. data/spec/punchblock/event/unjoined_spec.rb +7 -7
  114. data/spec/punchblock/ref_spec.rb +86 -9
  115. data/spec/punchblock/translator/asterisk/call_spec.rb +317 -154
  116. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +28 -5
  117. data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +15 -13
  118. data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +237 -0
  119. data/spec/punchblock/translator/asterisk/component/input_spec.rb +171 -14
  120. data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +652 -0
  121. data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +646 -0
  122. data/spec/punchblock/translator/asterisk/component/output_spec.rb +127 -77
  123. data/spec/punchblock/translator/asterisk/component/record_spec.rb +17 -8
  124. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +2 -2
  125. data/spec/punchblock/translator/asterisk/component_spec.rb +3 -7
  126. data/spec/punchblock/translator/asterisk_spec.rb +20 -24
  127. data/spec/punchblock/translator/freeswitch/call_spec.rb +103 -99
  128. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +17 -8
  129. data/spec/punchblock/translator/freeswitch/component/input_spec.rb +26 -14
  130. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +30 -52
  131. data/spec/punchblock/translator/freeswitch/component/record_spec.rb +23 -19
  132. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +18 -8
  133. data/spec/punchblock/translator/freeswitch/component_spec.rb +4 -8
  134. data/spec/punchblock/translator/freeswitch_spec.rb +11 -14
  135. data/spec/punchblock/uri_list_spec.rb +49 -0
  136. data/spec/punchblock_spec.rb +11 -1
  137. data/spec/spec_helper.rb +7 -11
  138. data/spec/support/mock_connection_with_event_handler.rb +1 -1
  139. metadata +104 -24
  140. data/lib/punchblock/header.rb +0 -9
  141. data/lib/punchblock/key_value_pair_node.rb +0 -51
  142. data/spec/punchblock/header_spec.rb +0 -11
@@ -0,0 +1,99 @@
1
+ # encoding: utf-8
2
+
3
+ require 'punchblock/translator/asterisk/unimrcp_app'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ class Asterisk
8
+ module Component
9
+ module MRCPRecogPrompt
10
+ UniMRCPError = Class.new Punchblock::Error
11
+
12
+ def execute
13
+ setup_defaults
14
+ validate
15
+ send_ref
16
+ execute_unimrcp_app
17
+ complete
18
+ rescue ChannelGoneError
19
+ call_ended
20
+ rescue UniMRCPError
21
+ complete_with_error 'Terminated due to UniMRCP error'
22
+ rescue RubyAMI::Error => e
23
+ complete_with_error "Terminated due to AMI error '#{e.message}'"
24
+ rescue OptionError => e
25
+ with_error 'option error', e.message
26
+ end
27
+
28
+ private
29
+
30
+ def validate
31
+ [:interrupt_on, :start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
32
+ raise OptionError, "A #{opt} value is unsupported on Asterisk." if output_node.send opt
33
+ end
34
+
35
+ raise OptionError, "An initial-timeout value must be -1 or a positive integer." if @initial_timeout < -1
36
+ raise OptionError, "An inter-digit-timeout value must be -1 or a positive integer." if @inter_digit_timeout < -1
37
+ end
38
+
39
+ def execute_app(app, *args)
40
+ UniMRCPApp.new(app, *args, unimrcp_app_options).execute @call
41
+ raise UniMRCPError if @call.channel_var('RECOG_STATUS') == 'ERROR'
42
+ end
43
+
44
+ def unimrcp_app_options
45
+ {uer: 1, b: (@component_node.barge_in == false ? 0 : 1)}.tap do |opts|
46
+ opts[:nit] = @initial_timeout if @initial_timeout > -1
47
+ opts[:dit] = @inter_digit_timeout if @inter_digit_timeout > -1
48
+ opts[:dttc] = input_node.terminator if input_node.terminator
49
+ yield opts
50
+ end
51
+ end
52
+
53
+ def setup_defaults
54
+ @initial_timeout = input_node.initial_timeout || -1
55
+ @inter_digit_timeout = input_node.inter_digit_timeout || -1
56
+ end
57
+
58
+ def grammars
59
+ input_node.grammars.map do |d|
60
+ if d.content_type
61
+ d.value.to_doc.to_s
62
+ else
63
+ d.url
64
+ end
65
+ end.join ','
66
+ end
67
+
68
+ def first_doc
69
+ output_node.render_documents.first
70
+ end
71
+
72
+ def audio_filename
73
+ first_doc.value.first
74
+ end
75
+
76
+ def output_node
77
+ @component_node.output
78
+ end
79
+
80
+ def input_node
81
+ @component_node.input
82
+ end
83
+
84
+ def complete
85
+ send_complete_event case @call.channel_var('RECOG_COMPLETION_CAUSE')
86
+ when '000'
87
+ nlsml = RubySpeech.parse URI.decode(@call.channel_var('RECOG_RESULT'))
88
+ Punchblock::Component::Input::Complete::Match.new nlsml: nlsml
89
+ when '001'
90
+ Punchblock::Component::Input::Complete::NoMatch.new
91
+ when '002'
92
+ Punchblock::Component::Input::Complete::NoInput.new
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'active_support/core_ext/string/filters'
4
+ require 'punchblock/translator/asterisk/unimrcp_app'
4
5
 
5
6
  module Punchblock
6
7
  module Translator
@@ -10,14 +11,17 @@ module Punchblock
10
11
  include StopByRedirect
11
12
 
12
13
  UnrenderableDocError = Class.new OptionError
14
+ UniMRCPError = Class.new Punchblock::Error
15
+ PlaybackError = Class.new Punchblock::Error
13
16
 
14
17
  def setup
15
18
  @media_engine = @call.translator.media_engine
16
19
  end
17
20
 
18
21
  def execute
19
- raise OptionError, 'An SSML document is required.' unless @component_node.ssml
20
- raise OptionError, 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :speech
22
+ raise OptionError, 'An SSML document is required.' unless @component_node.render_documents.first.value
23
+ raise OptionError, 'Only a single document is supported.' unless @component_node.render_documents.size == 1
24
+ raise OptionError, 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :voice
21
25
 
22
26
  [:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
23
27
  raise OptionError, "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
@@ -44,7 +48,7 @@ module Punchblock
44
48
  if interrupt
45
49
  output_component = current_actor
46
50
  call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
47
- output_component.stop_by_redirect Punchblock::Component::Output::Complete::Success.new
51
+ output_component.stop_by_redirect finish_reason
48
52
  end
49
53
  end
50
54
 
@@ -52,10 +56,12 @@ module Punchblock
52
56
 
53
57
  opts = early ? "#{path},noanswer" : path
54
58
  @call.execute_agi_command 'EXEC Playback', opts
59
+ raise PlaybackError if @call.channel_var('PLAYBACKSTATUS') == 'FAILED'
55
60
  when :unimrcp
56
61
  @call.send_progress if early
57
62
  send_ref
58
- @call.execute_agi_command 'EXEC MRCPSynth', escape_commas(escaped_doc), mrcpsynth_options
63
+ UniMRCPApp.new('MRCPSynth', render_doc, mrcpsynth_options).execute @call
64
+ raise UniMRCPError if @call.channel_var('SYNTHSTATUS') == 'ERROR'
59
65
  when :swift
60
66
  @call.send_progress if early
61
67
  send_ref
@@ -63,7 +69,13 @@ module Punchblock
63
69
  else
64
70
  raise OptionError, "The renderer #{rendering_engine} is unsupported."
65
71
  end
66
- send_success
72
+ send_finish
73
+ rescue ChannelGoneError
74
+ call_ended
75
+ rescue PlaybackError
76
+ complete_with_error 'Terminated due to playback error'
77
+ rescue UniMRCPError
78
+ complete_with_error 'Terminated due to UniMRCP error'
67
79
  rescue RubyAMI::Error => e
68
80
  complete_with_error "Terminated due to AMI error '#{e.message}'"
69
81
  rescue UnrenderableDocError => e
@@ -75,10 +87,10 @@ module Punchblock
75
87
  private
76
88
 
77
89
  def filenames
78
- @filenames ||= @component_node.ssml.children.map do |node|
90
+ @filenames ||= render_doc.children.map do |node|
79
91
  case node
80
92
  when RubySpeech::SSML::Audio
81
- node.src
93
+ node.src.sub('file://', '')
82
94
  when String
83
95
  raise if node.include?(' ')
84
96
  node
@@ -90,34 +102,30 @@ module Punchblock
90
102
  raise UnrenderableDocError, 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.'
91
103
  end
92
104
 
93
- def escaped_doc
94
- @component_node.ssml.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
95
- end
96
-
97
- def escape_commas(text)
98
- text.gsub(',', '\\,')
105
+ def render_doc
106
+ @component_node.render_documents.first.value
99
107
  end
100
108
 
101
109
  def mrcpsynth_options
102
- [].tap do |opts|
103
- opts << 'i=any' if [:any, :dtmf].include? @component_node.interrupt_on
104
- opts << "v=#{@component_node.voice}" if @component_node.voice
105
- end.join '&'
110
+ {}.tap do |opts|
111
+ opts[:i] = 'any' if [:any, :dtmf].include? @component_node.interrupt_on
112
+ opts[:v] = @component_node.voice if @component_node.voice
113
+ end
106
114
  end
107
115
 
108
116
  def swift_doc
109
- doc = escaped_doc
117
+ doc = render_doc.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
110
118
  doc << "|1|1" if [:any, :dtmf].include? @component_node.interrupt_on
111
119
  doc.insert 0, "#{@component_node.voice}^" if @component_node.voice
112
120
  doc
113
121
  end
114
122
 
115
- def send_success
116
- send_complete_event success_reason
123
+ def send_finish
124
+ send_complete_event finish_reason
117
125
  end
118
126
 
119
- def success_reason
120
- Punchblock::Component::Output::Complete::Success.new
127
+ def finish_reason
128
+ Punchblock::Component::Output::Complete::Finish.new
121
129
  end
122
130
  end
123
131
  end
@@ -27,8 +27,6 @@ module Punchblock
27
27
  component.finished
28
28
  end
29
29
 
30
- send_ref
31
-
32
30
  if @component_node.start_beep
33
31
  @call.execute_agi_command 'STREAM FILE', 'beep', '""'
34
32
  end
@@ -39,8 +37,12 @@ module Punchblock
39
37
  ami_client.send_action 'StopMonitor', 'Channel' => call.channel
40
38
  end
41
39
  end
40
+
41
+ send_ref
42
+ rescue ChannelGoneError
43
+ set_node_response ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{call_id}", call_id)
42
44
  rescue RubyAMI::Error => e
43
- complete_with_error "Terminated due to AMI error '#{e.message}'"
45
+ with_error :platform_error, "Terminated due to AMI error '#{e.message}'"
44
46
  rescue OptionError => e
45
47
  with_error 'option error', e.message
46
48
  end
@@ -63,7 +65,7 @@ module Punchblock
63
65
  end
64
66
 
65
67
  def finished
66
- send_complete_event(@complete_reason || success_reason)
68
+ send_complete_event(@complete_reason || max_duration_reason)
67
69
  end
68
70
 
69
71
  private
@@ -80,8 +82,8 @@ module Punchblock
80
82
  Punchblock::Event::Complete::Stop.new
81
83
  end
82
84
 
83
- def success_reason
84
- Punchblock::Component::Record::Complete::Success.new
85
+ def max_duration_reason
86
+ Punchblock::Component::Record::Complete::MaxDuration.new
85
87
  end
86
88
 
87
89
  def send_complete_event(reason)
@@ -7,8 +7,12 @@ module Punchblock
7
7
  extend ActiveSupport::Autoload
8
8
 
9
9
  autoload :Asterisk
10
+ autoload :ComposedPrompt
10
11
  autoload :Input
11
12
  autoload :Output
13
+ autoload :MRCPPrompt
14
+ autoload :MRCPNativePrompt
15
+ autoload :MRCPRecogPrompt
12
16
  autoload :Record
13
17
  autoload :StopByRedirect
14
18
 
@@ -36,10 +40,7 @@ module Punchblock
36
40
  def send_complete_event(reason, recording = nil, should_terminate = true)
37
41
  return if @complete
38
42
  @complete = true
39
- event = Punchblock::Event::Complete.new.tap do |c|
40
- c.reason = reason
41
- c << recording if recording
42
- end
43
+ event = Punchblock::Event::Complete.new reason: reason, recording: recording
43
44
  send_event event
44
45
  terminate if should_terminate
45
46
  end
@@ -73,7 +74,7 @@ module Punchblock
73
74
  end
74
75
 
75
76
  def send_ref
76
- set_node_response Ref.new :id => id
77
+ set_node_response Ref.new uri: id
77
78
  end
78
79
 
79
80
  def with_error(name, text)
@@ -0,0 +1,26 @@
1
+ require 'punchblock/translator/asterisk/agi_app'
2
+ require 'active_support/core_ext/string/filters'
3
+
4
+ module Punchblock
5
+ module Translator
6
+ class Asterisk
7
+ class UniMRCPApp
8
+ def initialize(app, *args, options)
9
+ args.map! { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }
10
+ args << prepare_options(options)
11
+ @agi_app = AGIApp.new(app, *args)
12
+ end
13
+
14
+ def execute(call)
15
+ @agi_app.execute call
16
+ end
17
+
18
+ private
19
+
20
+ def prepare_options(options)
21
+ options.map { |o| o.join '=' }.join '&'
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -8,10 +8,14 @@ module Punchblock
8
8
  class Asterisk
9
9
  include Celluloid
10
10
 
11
+ # Indicates that a command was executed against a channel which no longer exists
12
+ ChannelGoneError = Class.new Punchblock::Error
13
+
11
14
  extend ActiveSupport::Autoload
12
15
 
13
16
  autoload :AGICommand
14
17
  autoload :Call
18
+ autoload :Channel
15
19
  autoload :Component
16
20
 
17
21
  attr_reader :ami_client, :connection, :media_engine, :calls
@@ -20,15 +24,13 @@ module Punchblock
20
24
  REDIRECT_EXTENSION = '1'
21
25
  REDIRECT_PRIORITY = '1'
22
26
 
23
- CHANNEL_NORMALIZATION_REGEXP = /^(?<prefix>Bridge\/)*(?<channel>[^<>]*)(?<suffix><.*>)*$/.freeze
24
- EVENTS_ALLOWED_BRIDGED = %w{agiexec asyncagi}
27
+ EVENTS_ALLOWED_BRIDGED = %w{AGIExec AsyncAGI}
25
28
 
26
29
  trap_exit :actor_died
27
30
 
28
31
  def initialize(ami_client, connection, media_engine = nil)
29
32
  @ami_client, @connection, @media_engine = ami_client, connection, media_engine
30
33
  @calls, @components, @channel_to_call_id = {}, {}, {}
31
- @fully_booted_count = 0
32
34
  end
33
35
 
34
36
  def register_call(call)
@@ -36,9 +38,9 @@ module Punchblock
36
38
  @calls[call.id] ||= call
37
39
  end
38
40
 
39
- def deregister_call(call)
40
- @channel_to_call_id.delete call.channel
41
- @calls.delete call.id
41
+ def deregister_call(id, channel)
42
+ @channel_to_call_id.delete channel
43
+ @calls.delete id
42
44
  end
43
45
 
44
46
  def call_with_id(call_id)
@@ -46,7 +48,7 @@ module Punchblock
46
48
  end
47
49
 
48
50
  def call_for_channel(channel)
49
- call_with_id @channel_to_call_id[channel.match(CHANNEL_NORMALIZATION_REGEXP)[:channel]]
51
+ call_with_id @channel_to_call_id[Channel.new(channel).name]
50
52
  end
51
53
 
52
54
  def register_component(component)
@@ -65,7 +67,7 @@ module Punchblock
65
67
  def handle_ami_event(event)
66
68
  return unless event.is_a? RubyAMI::Event
67
69
 
68
- if event.name.downcase == "fullybooted"
70
+ if event.name == 'FullyBooted'
69
71
  handle_pb_event Connection::Connected.new
70
72
  run_at_fully_booted
71
73
  return
@@ -76,7 +78,7 @@ module Punchblock
76
78
  ami_dispatch_to_or_create_call event
77
79
 
78
80
  unless ami_event_known_call?(event)
79
- handle_pb_event Event::Asterisk::AMI::Event.new(:name => event.name, :attributes => event.headers)
81
+ handle_pb_event Event::Asterisk::AMI::Event.new(name: event.name, headers: event.headers)
80
82
  end
81
83
  end
82
84
  exclusive :handle_ami_event
@@ -171,24 +173,24 @@ module Punchblock
171
173
  end
172
174
 
173
175
  def ami_dispatch_to_or_create_call(event)
174
- if ami_event_known_call?(event)
175
- channels_for_ami_event(event).each do |channel|
176
- call = call_for_channel channel
177
- if call
178
- if channel_is_bridged?(channel)
179
- call.async.process_ami_event event if EVENTS_ALLOWED_BRIDGED.include?(event.name.downcase)
180
- else
181
- call.async.process_ami_event event
182
- end
183
- end
176
+ calls_for_event = channels_for_ami_event(event).inject({}) do |h, channel|
177
+ call = call_for_channel channel
178
+ h[channel] = call if call
179
+ h
180
+ end
181
+
182
+ if !calls_for_event.empty?
183
+ calls_for_event.each_pair do |channel, call|
184
+ next if channel.bridged? && !EVENTS_ALLOWED_BRIDGED.include?(event.name)
185
+ call.async.process_ami_event event
184
186
  end
185
- elsif event.name.downcase == "asyncagi" && event['SubEvent'] == "Start"
187
+ elsif event.name == "AsyncAGI" && event['SubEvent'] == "Start"
186
188
  handle_async_agi_start_event event
187
189
  end
188
190
  end
189
191
 
190
192
  def channels_for_ami_event(event)
191
- [event['Channel'], event['Channel1'], event['Channel2']].compact
193
+ [event['Channel'], event['Channel1'], event['Channel2']].compact.map { |channel| Channel.new(channel) }
192
194
  end
193
195
 
194
196
  def ami_event_known_call?(event)
@@ -197,18 +199,12 @@ module Punchblock
197
199
  (event['Channel2'] && call_for_channel(event['Channel2']))
198
200
  end
199
201
 
200
- def channel_is_bridged?(channel)
201
- matches = channel.match CHANNEL_NORMALIZATION_REGEXP
202
- matches[:prefix] || matches[:suffix]
203
- end
204
-
205
202
  def handle_async_agi_start_event(event)
206
203
  env = RubyAMI::AsyncAGIEnvironmentParser.new(event['Env']).to_hash
207
204
 
208
205
  return if env[:agi_extension] == 'h' || env[:agi_type] == 'Kill'
209
206
 
210
- call = Call.new event['Channel'], current_actor, ami_client, connection, env
211
- link call
207
+ call = Call.new_link event['Channel'], current_actor, ami_client, connection, env
212
208
  register_call call
213
209
  call.async.send_offer
214
210
  end
@@ -3,42 +3,45 @@
3
3
  module Punchblock
4
4
  module Translator
5
5
  class DTMFRecognizer
6
- def initialize(responder, grammar, initial_timeout = nil, inter_digit_timeout = nil)
6
+ def initialize(responder, grammar, initial_timeout = nil, inter_digit_timeout = nil, terminator = nil)
7
7
  @responder = responder
8
- self.grammar = grammar
9
8
  self.initial_timeout = initial_timeout || -1
10
9
  self.inter_digit_timeout = inter_digit_timeout || -1
10
+ @terminator = terminator
11
+ @finished = false
11
12
 
13
+ @matcher = RubySpeech::GRXML::Matcher.new RubySpeech::GRXML.import(grammar.to_s)
12
14
  @buffer = ""
13
-
14
- begin_initial_timer @initial_timeout/1000 unless @initial_timeout == -1
15
15
  end
16
16
 
17
17
  def <<(digit)
18
- @buffer << digit
19
18
  cancel_initial_timer
20
- case (match = @grammar.match @buffer.dup)
21
- when RubySpeech::GRXML::Match
22
- @responder.match match.mode, match.confidence, match.utterance, match.interpretation
19
+ @buffer << digit unless terminating?(digit)
20
+ case (match = get_match)
23
21
  when RubySpeech::GRXML::NoMatch
24
- @responder.nomatch
22
+ finalize :nomatch
23
+ when RubySpeech::GRXML::MaxMatch
24
+ finalize :match, match
25
+ when RubySpeech::GRXML::Match
26
+ finalize :match, match if terminating?(digit)
25
27
  when RubySpeech::GRXML::PotentialMatch
26
- reset_inter_digit_timer
28
+ finalize :nomatch if terminating?(digit)
27
29
  end
30
+ reset_inter_digit_timer unless @finished
28
31
  end
29
32
 
30
- def finalize
31
- cancel_initial_timer
32
- cancel_inter_digit_timer
33
+ def start_timers
34
+ begin_initial_timer @initial_timeout/1000 unless @initial_timeout == -1
33
35
  end
34
36
 
35
37
  private
36
38
 
37
- def grammar=(other)
38
- @grammar = RubySpeech::GRXML.import other.to_s
39
- @grammar.inline!
40
- @grammar.tokenize!
41
- @grammar.normalize_whitespace
39
+ def terminating?(digit)
40
+ digit == @terminator
41
+ end
42
+
43
+ def get_match
44
+ @matcher.match @buffer.dup
42
45
  end
43
46
 
44
47
  def after(*args, &block)
@@ -57,7 +60,7 @@ module Punchblock
57
60
 
58
61
  def begin_initial_timer(timeout)
59
62
  @initial_timer = after timeout do
60
- @responder.noinput
63
+ finalize :noinput
61
64
  end
62
65
  end
63
66
 
@@ -71,7 +74,12 @@ module Punchblock
71
74
  return if @inter_digit_timeout == -1
72
75
  @inter_digit_timer ||= begin
73
76
  after @inter_digit_timeout/1000 do
74
- @responder.nomatch
77
+ case (match = get_match)
78
+ when RubySpeech::GRXML::Match
79
+ finalize :match, match
80
+ else
81
+ finalize :nomatch
82
+ end
75
83
  end
76
84
  end
77
85
  @inter_digit_timer.reset
@@ -82,6 +90,17 @@ module Punchblock
82
90
  @inter_digit_timer.cancel
83
91
  @inter_digit_timer = nil
84
92
  end
93
+
94
+ def finalize(match_type, match = nil)
95
+ cancel_initial_timer
96
+ cancel_inter_digit_timer
97
+ if match
98
+ @responder.send match_type, match
99
+ else
100
+ @responder.send match_type
101
+ end
102
+ @finished = true
103
+ end
85
104
  end
86
105
  end
87
106
  end
@@ -14,9 +14,10 @@ module Punchblock
14
14
  HANGUP_CAUSE_TO_END_REASON = Hash.new :error
15
15
 
16
16
  HANGUP_CAUSE_TO_END_REASON['USER_BUSY'] = :busy
17
+ HANGUP_CAUSE_TO_END_REASON['MANAGER_REQUEST'] = :hangup_command
17
18
 
18
19
  %w{
19
- NORMAL_CLEARING ORIGINATOR_CANCEL SYSTEM_SHUTDOWN MANAGER_REQUEST
20
+ NORMAL_CLEARING ORIGINATOR_CANCEL SYSTEM_SHUTDOWN
20
21
  BLIND_TRANSFER ATTENDED_TRANSFER PICKED_OFF NORMAL_UNSPECIFIED
21
22
  }.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :hangup }
22
23
 
@@ -91,16 +92,16 @@ module Punchblock
91
92
  command = @pending_joins[event[:other_leg_unique_id]]
92
93
  command.response = true if command
93
94
 
94
- other_call_id = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
95
- send_pb_event Event::Joined.new(:call_id => other_call_id)
95
+ other_call_uri = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
96
+ send_pb_event Event::Joined.new(:call_uri => other_call_uri)
96
97
  end
97
98
 
98
99
  register_handler :es, :event_name => 'CHANNEL_UNBRIDGE' do |event|
99
100
  command = @pending_unjoins[event[:other_leg_unique_id]]
100
101
  command.response = true if command
101
102
 
102
- other_call_id = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
103
- send_pb_event Event::Unjoined.new(:call_id => other_call_id)
103
+ other_call_uri = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
104
+ send_pb_event Event::Unjoined.new(:call_uri => other_call_uri)
104
105
  end
105
106
 
106
107
  register_handler :es, [:has_key?, :scope_variable_punchblock_component_id] => true do |event|
@@ -145,8 +146,8 @@ module Punchblock
145
146
  options[:origination_caller_id_number] = "'#{cid_number}'" if cid_number.present?
146
147
  options[:origination_caller_id_name] = "'#{cid_name}'" if cid_name.present?
147
148
  options[:originate_timeout] = dial_command.timeout/1000 if dial_command.timeout
148
- dial_command.headers.each do |header|
149
- options["sip_h_#{header.name}"] = "'#{header.value}'"
149
+ dial_command.headers.each do |name, value|
150
+ options["sip_h_#{name}"] = "'#{value}'"
150
151
  end
151
152
  opts = options.inject([]) do |a, (k, v)|
152
153
  a << "#{k}=#{v}"
@@ -154,7 +155,7 @@ module Punchblock
154
155
 
155
156
  stream.bgapi "originate {#{opts}}#{dial_command.to} &park()"
156
157
 
157
- dial_command.response = Ref.new :id => id
158
+ dial_command.response = Ref.new uri: id
158
159
  end
159
160
 
160
161
  def outbound?
@@ -195,10 +196,10 @@ module Punchblock
195
196
  hangup
196
197
  command.response = true
197
198
  when Command::Join
198
- @pending_joins[command.call_id] = command
199
- uuid_foo :bridge, command.call_id
199
+ @pending_joins[command.call_uri] = command
200
+ uuid_foo :bridge, command.call_uri
200
201
  when Command::Unjoin
201
- @pending_unjoins[command.call_id] = command
202
+ @pending_unjoins[command.call_uri] = command
202
203
  uuid_foo :transfer, '-both park inline'
203
204
  when Command::Reject
204
205
  hangup REJECT_TO_HANGUP_REASON[command.reason]
@@ -222,7 +223,7 @@ module Punchblock
222
223
  end
223
224
  end
224
225
 
225
- def hangup(reason = 'NORMAL_CLEARING')
226
+ def hangup(reason = 'MANAGER_REQUEST')
226
227
  sendmsg :call_command => 'hangup', :hangup_cause => reason
227
228
  end
228
229
 
@@ -244,7 +245,7 @@ module Punchblock
244
245
 
245
246
  def send_end_event(reason)
246
247
  send_pb_event Event::End.new(:reason => reason)
247
- translator.deregister_call current_actor
248
+ translator.deregister_call id
248
249
  terminate
249
250
  end
250
251
 
@@ -268,7 +269,7 @@ module Punchblock
268
269
 
269
270
  def headers
270
271
  es_env.to_a.inject({}) do |accumulator, element|
271
- accumulator[('x_' + element[0].to_s).to_sym] = element[1] || ''
272
+ accumulator['X-' + element[0].to_s] = element[1] || ''
272
273
  accumulator
273
274
  end
274
275
  end
@@ -35,20 +35,21 @@ module Punchblock
35
35
  end
36
36
 
37
37
  def validate
38
- raise OptionError, 'An SSML document is required.' unless @component_node.ssml
38
+ raise OptionError, 'An SSML document is required.' unless @component_node.render_documents.first.value
39
+ raise OptionError, 'Only a single document is supported.' unless @component_node.render_documents.size == 1
39
40
 
40
41
  [:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
41
42
  raise OptionError, "A #{opt} value is unsupported." if @component_node.send opt
42
43
  end
43
44
 
44
45
  case @component_node.interrupt_on
45
- when :speech, :dtmf, :any
46
+ when :voice, :dtmf, :any
46
47
  raise OptionError, "An interrupt-on value of #{@component_node.interrupt_on} is unsupported."
47
48
  end
48
49
  end
49
50
 
50
- def success_reason
51
- Punchblock::Component::Output::Complete::Success.new
51
+ def finish_reason
52
+ Punchblock::Component::Output::Complete::Finish.new
52
53
  end
53
54
  end
54
55
  end