punchblock 1.9.4 → 2.0.0.beta1

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