punchblock 2.1.1 → 2.2.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 (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