punchblock 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -1
  3. data/CHANGELOG.md +7 -0
  4. data/lib/punchblock.rb +1 -1
  5. data/lib/punchblock/component.rb +2 -0
  6. data/lib/punchblock/component/input.rb +9 -1
  7. data/lib/punchblock/component/output.rb +1 -1
  8. data/lib/punchblock/component/receive_fax.rb +24 -0
  9. data/lib/punchblock/component/send_fax.rb +62 -0
  10. data/lib/punchblock/connection/asterisk.rb +1 -1
  11. data/lib/punchblock/event/complete.rb +15 -1
  12. data/lib/punchblock/translator/asterisk.rb +30 -15
  13. data/lib/punchblock/translator/asterisk/call.rb +13 -27
  14. data/lib/punchblock/translator/asterisk/component.rb +4 -7
  15. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +1 -5
  16. data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +8 -9
  17. data/lib/punchblock/translator/asterisk/component/input.rb +2 -3
  18. data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +9 -9
  19. data/lib/punchblock/translator/asterisk/component/output.rb +134 -39
  20. data/lib/punchblock/translator/asterisk/component/record.rb +2 -3
  21. data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +2 -3
  22. data/lib/punchblock/translator/dtmf_recognizer.rb +2 -4
  23. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +6 -1
  24. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
  25. data/lib/punchblock/translator/freeswitch/component/output.rb +12 -10
  26. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +1 -1
  27. data/lib/punchblock/version.rb +1 -1
  28. data/spec/punchblock/component/input_spec.rb +91 -0
  29. data/spec/punchblock/component/output_spec.rb +1 -2
  30. data/spec/punchblock/component/receive_fax_spec.rb +111 -0
  31. data/spec/punchblock/component/send_fax_spec.rb +110 -0
  32. data/spec/punchblock/connection/asterisk_spec.rb +1 -1
  33. data/spec/punchblock/translator/asterisk/call_spec.rb +53 -79
  34. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +0 -3
  35. data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +1 -1
  36. data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +2 -2
  37. data/spec/punchblock/translator/asterisk/component/input_spec.rb +6 -6
  38. data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +3 -3
  39. data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +9 -11
  40. data/spec/punchblock/translator/asterisk/component/output_spec.rb +902 -28
  41. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +1 -1
  42. data/spec/punchblock/translator/asterisk/component_spec.rb +2 -9
  43. data/spec/punchblock/translator/asterisk_spec.rb +42 -94
  44. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +5 -5
  45. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +7 -3
  46. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +17 -5
  47. metadata +67 -61
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d1695be81bb0d8afb829d648392c00705f8ec56a
4
- data.tar.gz: 1a2fd855be244f16d1858b00aeca5309bd865af8
3
+ metadata.gz: 1152b11b0e04b0b908129ed719d27c5d82c3e840
4
+ data.tar.gz: 8680c53051c70725c6b3aeb1b1fd05af8e416ceb
5
5
  SHA512:
6
- metadata.gz: 8fc40e0dbcad7bf5435a1fff4e37a3237c0768cea38bab9bdacc5bcce20df41baf74cff01160a3a39db566198c92a049461ebcd7e9f8152ace80d05b65be23a4
7
- data.tar.gz: 43917ff261fa61ddfce65cc0260e07a04d7b673d59bce5fc73149ab8e29056e9fbb04747bafcd8fba949274ad4fef45453e7ba36347d2da3d733997b7cff5434
6
+ metadata.gz: 26766a491387d61395c7ebb9cd6302ee7e0ac0c981ec6043b83f5ee098cb11684711b147e4094b2ad7da1c20d23fa9a2e5f39521f7197e90eeb8a1e4d360da92
7
+ data.tar.gz: e214e5d8e43088da4105ce7c3f4b20cba314440208cddfcf19887d6386ac517598e9cd7c151735e851abc20c6a93f1d73af589330ad40f71d14bb33ab866af7b
data/.travis.yml CHANGED
@@ -3,8 +3,9 @@ rvm:
3
3
  - 1.9.2
4
4
  - 1.9.3
5
5
  - 2.0.0
6
+ - 2.1.0
6
7
  - jruby-19mode
7
- - rbx-19mode
8
+ - rbx-2.1.1
8
9
  - ruby-head
9
10
  matrix:
10
11
  allow_failures:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # [develop](https://github.com/adhearsion/punchblock)
2
2
 
3
+ # [v2.2.0](https://github.com/adhearsion/punchblock/compare/v2.1.1...v2.2.0) - [2014-01-15](https://rubygems.org/gems/punchblock/versions/2.2.0)
4
+ * Feature: Support Rayo CPA and Fax specifications
5
+ * Feature: Implement a simple filter for AMI events -> Rayo events in `Punchblock::Translator::Asterisk.event_filter=`
6
+ * Feature: Output on Asterisk supports a new `:native_or_unimrcp` renderer which allows for fallback of output rendering from Asterisk native when possible, to a TTS engine when it's not.
7
+ * Feature: Support output `repeat-times` on Asterisk.
8
+ * Bugfix: Remove per-call/component actors from Asterisk translator for performance/stability super-charge
9
+
3
10
  # [v2.1.1](https://github.com/adhearsion/punchblock/compare/v2.1.0...v2.1.1) - [2013-12-19](https://rubygems.org/gems/punchblock/versions/2.1.1)
4
11
  * Bugfix: Allow sending string SSML docs via Rayo
5
12
  * Bugfix: Ensure that joined calls on Asterisk do not have implicitly linked lifecycles. Previously, joinees would be hungup when the joiner exited the bridge.
data/lib/punchblock.rb CHANGED
@@ -75,7 +75,7 @@ module Punchblock
75
75
  RAYO_VERSION = '1'
76
76
  RAYO_NAMESPACES = {:core => [BASE_RAYO_NAMESPACE, RAYO_VERSION].compact.join(':')}
77
77
 
78
- [:ext, :record, :output, :input, :prompt].each do |ns|
78
+ [:ext, :record, :output, :input, :prompt, :cpa, :fax].each do |ns|
79
79
  RAYO_NAMESPACES[ns] = [BASE_RAYO_NAMESPACE, ns.to_s, RAYO_VERSION].compact.join(':')
80
80
  RAYO_NAMESPACES[:"#{ns}_complete"] = [BASE_RAYO_NAMESPACE, ns.to_s, 'complete', RAYO_VERSION].compact.join(':')
81
81
  end
@@ -9,7 +9,9 @@ module Punchblock
9
9
  autoload :Input
10
10
  autoload :Output
11
11
  autoload :Prompt
12
+ autoload :ReceiveFax
12
13
  autoload :Record
14
+ autoload :SendFax
13
15
  autoload :Stop
14
16
 
15
17
  InvalidActionError = Class.new StandardError
@@ -98,7 +98,7 @@ module Punchblock
98
98
  end
99
99
 
100
100
  def rayo_children(root)
101
- root.cdata value
101
+ root.cdata value if value
102
102
  end
103
103
 
104
104
  private
@@ -108,6 +108,14 @@ module Punchblock
108
108
  end
109
109
  end
110
110
 
111
+ class Signal < Event::Complete::Reason
112
+ register :signal, :cpa
113
+
114
+ attribute :type, String
115
+ attribute :duration, Integer
116
+ attribute :value, String
117
+ end
118
+
111
119
  class Complete
112
120
  class Match < Event::Complete::Reason
113
121
  register :match, :input_complete
@@ -80,7 +80,7 @@ module Punchblock
80
80
  attribute :start_offset, Integer
81
81
 
82
82
  # @return [true, false] Indicates wether or not the component should be started in a paused state to be resumed at a later time.
83
- attribute :start_paused, Boolean, default: false
83
+ attribute :start_paused, Boolean
84
84
 
85
85
  # @return [Integer] Indicates the duration of silence that should space repeats of the rendered document.
86
86
  attribute :repeat_interval, Integer
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ module Punchblock
4
+ module Component
5
+ class ReceiveFax < ComponentNode
6
+ register :receivefax, :fax
7
+
8
+ class Fax < RayoNode
9
+ register :fax, :fax_complete
10
+
11
+ attribute :url, String
12
+ attribute :resolution, String
13
+ attribute :pages, Integer
14
+ attribute :size, Integer
15
+ end
16
+
17
+ class Complete
18
+ class Finish < Event::Complete::Reason
19
+ register :finish, :fax_complete
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+
3
+ module Punchblock
4
+ module Component
5
+ class SendFax < ComponentNode
6
+ register :sendfax, :fax
7
+
8
+ class FaxDocument < RayoNode
9
+ register :document, :fax
10
+
11
+ attribute :url, String
12
+ attribute :identity, String
13
+ attribute :header, String
14
+ attribute :pages, String
15
+
16
+ def inherit(xml_node)
17
+ super
18
+ if pages = xml_node[:pages]
19
+ self.pages = pages.split(',').map { |p| p.include?('-') ? Range.new(*p.split('-').map(&:to_i)) : p.to_i }
20
+ end
21
+ self
22
+ end
23
+
24
+ def rayo_attributes
25
+ {
26
+ 'url' => url,
27
+ 'identity' => identity,
28
+ 'header' => header,
29
+ 'pages' => rayo_pages
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ def rayo_pages
36
+ pages ? pages.map { |p| p.is_a?(Range) ? "#{p.min}-#{p.max}" : p }.join(',') : nil
37
+ end
38
+ end
39
+
40
+ def inherit(xml_node)
41
+ document_nodes = xml_node.xpath 'ns:document', ns: self.class.registered_ns
42
+ self.render_documents = document_nodes.to_a.map { |node| FaxDocument.from_xml node }
43
+
44
+ super
45
+ end
46
+
47
+ def rayo_children(root)
48
+ render_documents.each do |render_document|
49
+ render_document.to_rayo root.parent
50
+ end
51
+ super
52
+ end
53
+
54
+ # @return [Document] the document to render
55
+ attribute :render_documents, Array[FaxDocument], default: []
56
+
57
+ def render_document=(other)
58
+ self.render_documents = [other].compact
59
+ end
60
+ end
61
+ end
62
+ end
@@ -21,7 +21,7 @@ module Punchblock
21
21
  end
22
22
 
23
23
  def stop
24
- translator.async.shutdown
24
+ translator.terminate
25
25
  ami_client.terminate
26
26
  end
27
27
 
@@ -9,6 +9,9 @@ module Punchblock
9
9
 
10
10
  attribute :recording
11
11
 
12
+ attribute :fax
13
+ attribute :fax_metadata, Hash, default: {}
14
+
12
15
  def inherit(xml_node)
13
16
  if reason_node = xml_node.at_xpath('*')
14
17
  self.reason = RayoNode.from_xml(reason_node).tap do |reason|
@@ -24,11 +27,22 @@ module Punchblock
24
27
  end
25
28
  end
26
29
 
30
+ if fax_node = xml_node.at_xpath('//ns:fax', ns: RAYO_NAMESPACES[:fax_complete])
31
+ self.fax = RayoNode.from_xml(fax_node).tap do |fax|
32
+ fax.target_call_id = target_call_id
33
+ fax.component_id = component_id
34
+ end
35
+ end
36
+
37
+ xml_node.xpath('//ns:metadata', ns: RAYO_NAMESPACES[:fax_complete]).each do |md|
38
+ fax_metadata[md['name']] = md['value']
39
+ end
40
+
27
41
  super
28
42
  end
29
43
 
30
44
  class Reason < RayoNode
31
- attribute :name
45
+ attribute :name, Symbol, default: ->(node,_) { node.class.registered_name.to_sym }
32
46
 
33
47
  def inherit(xml_node)
34
48
  self.name = xml_node.name.to_sym
@@ -28,6 +28,23 @@ module Punchblock
28
28
 
29
29
  trap_exit :actor_died
30
30
 
31
+ # Set the AMI event filter to be applied to incoming AMI events. A truthy return value will send the event via Rayo to the client (Adhearsion).
32
+ #
33
+ # @param [#[<RubyAMI::Event>]] filter
34
+ #
35
+ # @example A lambda
36
+ # Punchblock::Translator::Asterisk.event_filter = ->(event) { event.name == 'AsyncAGI' }
37
+ #
38
+ def self.event_filter=(filter)
39
+ @event_filter = filter
40
+ end
41
+
42
+ def self.event_passes_filter?(event)
43
+ @event_filter ? !!@event_filter[event] : true
44
+ end
45
+
46
+ event_filter = nil
47
+
31
48
  def initialize(ami_client, connection)
32
49
  @ami_client, @connection = ami_client, connection
33
50
  @calls, @components, @channel_to_call_id = {}, {}, {}
@@ -59,11 +76,6 @@ module Punchblock
59
76
  @components[component_id]
60
77
  end
61
78
 
62
- def shutdown
63
- @calls.values.each { |call| call.async.shutdown }
64
- terminate
65
- end
66
-
67
79
  def handle_ami_event(event)
68
80
  return unless event.is_a? RubyAMI::Event
69
81
 
@@ -77,11 +89,10 @@ module Punchblock
77
89
 
78
90
  ami_dispatch_to_or_create_call event
79
91
 
80
- unless ami_event_known_call?(event)
92
+ if !ami_event_known_call?(event) && self.class.event_passes_filter?(event)
81
93
  handle_pb_event Event::Asterisk::AMI::Event.new(name: event.name, headers: event.headers)
82
94
  end
83
95
  end
84
- exclusive :handle_ami_event
85
96
 
86
97
  def handle_pb_event(event)
87
98
  connection.handle_event event
@@ -104,7 +115,11 @@ module Punchblock
104
115
 
105
116
  def execute_call_command(command)
106
117
  if call = call_with_id(command.target_call_id)
107
- call.async.execute_command command
118
+ begin
119
+ call.execute_command command
120
+ rescue => e
121
+ deregister_call call.id, call.channel
122
+ end
108
123
  else
109
124
  command.response = ProtocolError.new.setup :item_not_found, "Could not find a call with ID #{command.target_call_id}", command.target_call_id
110
125
  end
@@ -112,7 +127,7 @@ module Punchblock
112
127
 
113
128
  def execute_component_command(command)
114
129
  if (component = component_with_id(command.component_id))
115
- component.async.execute_command command
130
+ component.execute_command command
116
131
  else
117
132
  command.response = ProtocolError.new.setup :item_not_found, "Could not find a component with ID #{command.component_id}", command.target_call_id, command.component_id
118
133
  end
@@ -123,11 +138,11 @@ module Punchblock
123
138
  when Punchblock::Component::Asterisk::AMI::Action
124
139
  component = Component::Asterisk::AMIAction.new command, current_actor, ami_client
125
140
  register_component component
126
- component.async.execute
141
+ component.execute
127
142
  when Punchblock::Command::Dial
128
- call = Call.new_link command.to, current_actor, ami_client, connection
143
+ call = Call.new command.to, current_actor, ami_client, connection
129
144
  register_call call
130
- call.async.dial command
145
+ call.dial command
131
146
  else
132
147
  command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command"
133
148
  end
@@ -182,7 +197,7 @@ module Punchblock
182
197
  if !calls_for_event.empty?
183
198
  calls_for_event.each_pair do |channel, call|
184
199
  next if channel.bridged? && !EVENTS_ALLOWED_BRIDGED.include?(event.name)
185
- call.async.process_ami_event event
200
+ call.process_ami_event event
186
201
  end
187
202
  elsif event.name == "AsyncAGI" && event['SubEvent'] == "Start"
188
203
  handle_async_agi_start_event event
@@ -204,9 +219,9 @@ module Punchblock
204
219
 
205
220
  return if env[:agi_extension] == 'h' || env[:agi_type] == 'Kill'
206
221
 
207
- call = Call.new_link event['Channel'], current_actor, ami_client, connection, env
222
+ call = Call.new event['Channel'], current_actor, ami_client, connection, env
208
223
  register_call call
209
- call.async.send_offer
224
+ call.send_offer
210
225
  end
211
226
  end
212
227
  end
@@ -7,12 +7,8 @@ module Punchblock
7
7
  class Asterisk
8
8
  class Call
9
9
  include HasGuardedHandlers
10
- include Celluloid
11
10
  include DeadActorSafety
12
11
 
13
- extend ActorHasGuardedHandlers
14
- execute_guarded_handlers_on_receiver
15
-
16
12
  InvalidCommandError = Class.new Punchblock::Error
17
13
 
18
14
  OUTBOUND_CHANNEL_MATCH = /.* <(?<channel>.*)>/.freeze
@@ -29,8 +25,6 @@ module Punchblock
29
25
  HANGUP_CAUSE_TO_END_REASON[22] = :reject
30
26
  HANGUP_CAUSE_TO_END_REASON[102] = :timeout
31
27
 
32
- trap_exit :actor_died
33
-
34
28
  def initialize(channel, translator, ami_client, connection, agi_env = nil)
35
29
  @channel, @translator, @ami_client, @connection = channel, translator, ami_client, connection
36
30
  @agi_env = agi_env || {}
@@ -47,6 +41,10 @@ module Punchblock
47
41
  @components[component.id] ||= component
48
42
  end
49
43
 
44
+ def deregister_component(id)
45
+ @components.delete id
46
+ end
47
+
50
48
  def component_with_id(component_id)
51
49
  @components[component_id]
52
50
  end
@@ -56,10 +54,6 @@ module Punchblock
56
54
  send_pb_event offer_event
57
55
  end
58
56
 
59
- def shutdown
60
- terminate
61
- end
62
-
63
57
  def channel_var(variable)
64
58
  @channel_variables[variable] || fetch_channel_var(variable)
65
59
  end
@@ -113,7 +107,9 @@ module Punchblock
113
107
  end
114
108
 
115
109
  def process_ami_event(ami_event)
116
- send_pb_event Event::Asterisk::AMI::Event.new(name: ami_event.name, headers: ami_event.headers)
110
+ if Asterisk.event_passes_filter?(ami_event)
111
+ send_pb_event Event::Asterisk::AMI::Event.new(name: ami_event.name, headers: ami_event.headers)
112
+ end
117
113
 
118
114
  case ami_event.name
119
115
  when 'Hangup'
@@ -258,8 +254,6 @@ module Punchblock
258
254
  event = condition.wait
259
255
  return unless event
260
256
  agi.parse_result event
261
- rescue ChannelGoneError, RubyAMI::Error => e
262
- abort e
263
257
  end
264
258
 
265
259
  def logger_id
@@ -285,21 +279,14 @@ module Punchblock
285
279
  def handle_hangup_event(code = 16)
286
280
  reason = @hangup_cause || HANGUP_CAUSE_TO_END_REASON[code]
287
281
  @block_commands = true
288
- @components.dup.each_pair do |id, component|
289
- safe_from_dead_actors do
290
- component.call_ended if component.alive?
291
- end
282
+ @components.each_pair do |id, component|
283
+ component.call_ended
292
284
  end
293
285
  send_end_event reason, code
294
286
  end
295
287
 
296
- def actor_died(actor, reason)
297
- if id = @components.key(actor)
298
- @components.delete id
299
- return unless reason
300
- complete_event = Punchblock::Event::Complete.new :component_id => id, source_uri: id, :reason => Punchblock::Event::Complete::Error.new
301
- send_pb_event complete_event
302
- end
288
+ def after(*args, &block)
289
+ translator.after(*args, &block)
303
290
  end
304
291
 
305
292
  private
@@ -320,13 +307,12 @@ module Punchblock
320
307
  def send_end_event(reason, code = nil)
321
308
  send_pb_event Event::End.new(reason: reason, platform_code: code)
322
309
  translator.deregister_call id, channel
323
- terminate
324
310
  end
325
311
 
326
312
  def execute_component(type, command, options = {})
327
- type.new_link(command, current_actor).tap do |component|
313
+ type.new(command, self).tap do |component|
328
314
  register_component component
329
- component.async.execute
315
+ component.execute
330
316
  end
331
317
  end
332
318
 
@@ -17,14 +17,11 @@ module Punchblock
17
17
  autoload :StopByRedirect
18
18
 
19
19
  class Component
20
- include Celluloid
21
- include DeadActorSafety
22
-
23
20
  attr_reader :id, :call, :call_id
24
21
 
25
22
  def initialize(component_node, call = nil)
26
23
  @component_node, @call = component_node, call
27
- @call_id = safe_from_dead_actors { call.id } if call
24
+ @call_id = call.id if call
28
25
  @id = Punchblock.new_uuid
29
26
  @complete = false
30
27
  setup
@@ -37,19 +34,19 @@ module Punchblock
37
34
  command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command for component #{id}", call_id, id
38
35
  end
39
36
 
40
- def send_complete_event(reason, recording = nil, should_terminate = true)
37
+ def send_complete_event(reason, recording = nil)
41
38
  return if @complete
42
39
  @complete = true
43
40
  event = Punchblock::Event::Complete.new reason: reason, recording: recording
44
41
  send_event event
45
- terminate if should_terminate
42
+ call.deregister_component id if call
46
43
  end
47
44
 
48
45
  def send_event(event)
49
46
  event.component_id = id
50
47
  event.target_call_id = call_id
51
48
  event.source_uri = id
52
- safe_from_dead_actors { translator.handle_pb_event event }
49
+ translator.handle_pb_event event
53
50
  end
54
51
 
55
52
  def logger_id