punchblock 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. data/.travis.yml +3 -3
  2. data/CHANGELOG.md +23 -0
  3. data/lib/punchblock.rb +24 -0
  4. data/lib/punchblock/command/reject.rb +10 -2
  5. data/lib/punchblock/component/record.rb +16 -0
  6. data/lib/punchblock/core_ext/blather/stanza.rb +3 -1
  7. data/lib/punchblock/dead_actor_safety.rb +9 -0
  8. data/lib/punchblock/event/complete.rb +9 -11
  9. data/lib/punchblock/rayo_node.rb +4 -0
  10. data/lib/punchblock/translator/asterisk.rb +65 -22
  11. data/lib/punchblock/translator/asterisk/call.rb +49 -30
  12. data/lib/punchblock/translator/asterisk/component.rb +6 -8
  13. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +13 -20
  14. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +1 -1
  15. data/lib/punchblock/translator/asterisk/component/input.rb +3 -6
  16. data/lib/punchblock/translator/asterisk/component/output.rb +40 -45
  17. data/lib/punchblock/translator/asterisk/component/record.rb +1 -1
  18. data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +5 -2
  19. data/lib/punchblock/version.rb +1 -1
  20. data/punchblock.gemspec +5 -5
  21. data/spec/punchblock/command/reject_spec.rb +7 -1
  22. data/spec/punchblock/command_node_spec.rb +5 -2
  23. data/spec/punchblock/component/component_node_spec.rb +4 -0
  24. data/spec/punchblock/component/output_spec.rb +1 -1
  25. data/spec/punchblock/component/record_spec.rb +30 -0
  26. data/spec/punchblock/event/complete_spec.rb +10 -0
  27. data/spec/punchblock/translator/asterisk/call_spec.rb +191 -48
  28. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +6 -39
  29. data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +3 -3
  30. data/spec/punchblock/translator/asterisk/component/input_spec.rb +8 -3
  31. data/spec/punchblock/translator/asterisk/component/output_spec.rb +153 -46
  32. data/spec/punchblock/translator/asterisk/component/record_spec.rb +6 -5
  33. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +1 -2
  34. data/spec/punchblock/translator/asterisk/component_spec.rb +1 -0
  35. data/spec/punchblock/translator/asterisk_spec.rb +147 -12
  36. data/spec/punchblock_spec.rb +34 -0
  37. data/spec/spec_helper.rb +5 -1
  38. metadata +30 -20
data/.travis.yml CHANGED
@@ -2,8 +2,8 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.2
4
4
  - 1.9.3
5
- - jruby-19mode # JRuby in 1.9 mode
6
- - rbx-19mode # currently in active development, may or may not work for your project
7
- - ruby-head
5
+ # - jruby-19mode
6
+ # - rbx-19mode
7
+ # - ruby-head
8
8
  notifications:
9
9
  irc: "irc.freenode.org#adhearsion"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # [develop](https://github.com/adhearsion/punchblock)
2
2
 
3
+ # [v1.3.0](https://github.com/adhearsion/punchblock/compare/v1.2.0...v1.3.0) - [2012-07-22](https://rubygems.org/gems/punchblock/versions/1.3.0)
4
+ * Change: Asterisk output now uses Playback rather than STREAM FILE
5
+ * Feature: The recordings dir is now checked for existence on startup, and logs an error if it is not there. Asterisk only.
6
+ * Feature: Punchblock now logs an error if it was unable to add the redirect context on Asterisk on startup.
7
+ * Feature: Output component now exposes #recording and #recording_uri for easy access to record results.
8
+ * Feature: Early media support for Asterisk, using Progress to start an early media session
9
+ * Feature: Output component on Asterisk now supports early media. If the line is not answered, it runs Progress followed by Playback with noanswer.
10
+ * Feature: Record component on Asterisk now raises if called on an unanswered call
11
+ * Feature: Input component on Asterisk works the same whether the call is answered or unanswered
12
+ * Feature: AMI events are emitted to the relevant calls
13
+ * Feature: Simpler method of getting hold of a new client/connection
14
+ * Bugfix: AMI events are processed in order by the translator
15
+ * Bugfix: Asterisk calls and components are removed from registries when they die
16
+ * Bugfix: Commands for unknown calls/components respond with the correct `:item_not_found` name
17
+ * Bugfix: AMI events relevant to a particular call are emitted by that call to the client
18
+ * Bugfix: Asterisk calls send an error complete event for their dying components
19
+ * Bugfix: Asterisk translator sends an error end event for its dying calls
20
+ * Bugfix: Use the primitive version of AGI ANSWER, rather than an app
21
+ * Bugfix: Outbound calls which never begin progress on Asterisk end with an error
22
+ * Bugfix: Asterisk now responds correctly to unjoin commands
23
+ * Bugfix: Allow nil reject reasons
24
+ * Bugfix: Asterisk translator now does NOT answer the call automatically when Output, Input or Record are used.
25
+
3
26
  # [v1.2.0](https://github.com/adhearsion/punchblock/compare/v1.1.0...v1.2.0) - [2012-04-29](https://rubygems.org/gems/punchblock/versions/1.2.0)
4
27
  * Feature: Basic support for record component on Asterisk, using MixMonitor. Currently unsupported options include: start_paused, initial_timeout, final_timeout. Hints are additionally not supported, and recordings are stored on the * machine's local filesystem.
5
28
 
data/lib/punchblock.rb CHANGED
@@ -18,6 +18,7 @@ module Punchblock
18
18
  autoload :CommandNode
19
19
  autoload :Component
20
20
  autoload :Connection
21
+ autoload :DeadActorSafety
21
22
  autoload :DisconnectedError
22
23
  autoload :HasHeaders
23
24
  autoload :Header
@@ -39,6 +40,29 @@ module Punchblock
39
40
  def reset_logger
40
41
  @logger = NullObject.new
41
42
  end
43
+
44
+ #
45
+ # Get a new Punchblock client with a connection attached
46
+ #
47
+ # @param [Symbol] type the connection type (eg :XMPP, :asterisk)
48
+ # @param [Hash] options the options to pass to the connection (credentials, etc
49
+ #
50
+ # @return [Punchblock::Client] a punchblock client object
51
+ #
52
+ def client_with_connection(type, options)
53
+ connection = Connection.const_get(type.to_s.classify).new options
54
+ Client.new :connection => connection
55
+ rescue NameError
56
+ raise ArgumentError, "Connection type #{type.inspect} is not valid."
57
+ end
58
+
59
+ def new_uuid
60
+ SecureRandom.uuid
61
+ end
62
+
63
+ def jruby?
64
+ @jruby ||= !!(RUBY_PLATFORM =~ /java/)
65
+ end
42
66
  end
43
67
 
44
68
  ##
@@ -41,7 +41,8 @@ module Punchblock
41
41
  # @return [Symbol] the reason type for rejecting a call
42
42
  #
43
43
  def reason
44
- children.select { |c| c.is_a? Nokogiri::XML::Element }.first.name.to_sym
44
+ node = reason_node
45
+ node ? node.name.to_sym : nil
45
46
  end
46
47
 
47
48
  ##
@@ -56,12 +57,19 @@ module Punchblock
56
57
  raise ArgumentError, "Invalid Reason (#{reject_reason}), use: #{VALID_REASONS*' '}"
57
58
  end
58
59
  children.each(&:remove)
59
- self << RayoNode.new(reject_reason)
60
+ self << RayoNode.new(reject_reason) if reject_reason
60
61
  end
61
62
 
62
63
  def inspect_attributes # :nodoc:
63
64
  [:reason] + super
64
65
  end
66
+
67
+ private
68
+
69
+ def reason_node
70
+ node_children = children.select { |c| [Nokogiri::XML::Element, Niceogiri::XML::Node].any? { |k| c.is_a?(k) } }
71
+ node_children.first
72
+ end
65
73
  end # Reject
66
74
  end # Command
67
75
  end # Punchblock
@@ -188,6 +188,22 @@ module Punchblock
188
188
  end
189
189
  end
190
190
 
191
+ ##
192
+ # Directly returns the recording for the component
193
+ # @return [Punchblock::Component::Record::Recording] The recording object
194
+ #
195
+ def recording
196
+ complete_event.recording
197
+ end
198
+
199
+ ##
200
+ # Directly returns the recording URI for the component
201
+ # @return [String] The recording URI
202
+ #
203
+ def recording_uri
204
+ recording.uri
205
+ end
206
+
191
207
  class Pause < CommandNode # :nodoc:
192
208
  register :pause, :record
193
209
  end
@@ -7,8 +7,10 @@ module Blather
7
7
  # representing the Rayo command/event contained within the stanza
8
8
  #
9
9
  def rayo_node
10
- first_child = children.first
10
+ first_child = at_xpath '*'
11
11
  Punchblock::RayoNode.import first_child, nil, component_id if first_child
12
+ rescue Punchblock::RayoNode::InvalidNodeError
13
+ nil
12
14
  end
13
15
 
14
16
  ##
@@ -0,0 +1,9 @@
1
+ module Punchblock
2
+ module DeadActorSafety
3
+ def safe_from_dead_actors
4
+ yield
5
+ rescue Celluloid::DeadActorError => e
6
+ pb_logger.error e
7
+ end
8
+ end
9
+ end
@@ -10,12 +10,11 @@ module Punchblock
10
10
  register :complete, :ext
11
11
 
12
12
  def reason
13
- element = find_first('*')
14
- if element
15
- RayoNode.import(element).tap do |reason|
16
- reason.target_call_id = target_call_id
17
- reason.component_id = component_id
18
- end
13
+ element = find_first '*'
14
+ return unless element
15
+ RayoNode.import(element).tap do |reason|
16
+ reason.target_call_id = target_call_id
17
+ reason.component_id = component_id
19
18
  end
20
19
  end
21
20
 
@@ -26,11 +25,10 @@ module Punchblock
26
25
 
27
26
  def recording
28
27
  element = find_first('//ns:recording', :ns => RAYO_NAMESPACES[:record_complete])
29
- if element
30
- RayoNode.import(element).tap do |recording|
31
- recording.target_call_id = target_call_id
32
- recording.component_id = component_id
33
- end
28
+ return unless element
29
+ RayoNode.import(element).tap do |recording|
30
+ recording.target_call_id = target_call_id
31
+ recording.component_id = component_id
34
32
  end
35
33
  end
36
34
 
@@ -5,6 +5,8 @@ require 'niceogiri'
5
5
 
6
6
  module Punchblock
7
7
  class RayoNode < Niceogiri::XML::Node
8
+ InvalidNodeError = Class.new Punchblock::Error
9
+
8
10
  @@registrations = {}
9
11
 
10
12
  class_attribute :registered_ns, :registered_name
@@ -41,6 +43,7 @@ module Punchblock
41
43
  # @param [XML::Node] node the node to import
42
44
  # @return the appropriate object based on the node name and namespace
43
45
  def self.import(node, call_id = nil, component_id = nil)
46
+ node = Nokogiri::XML(node.to_xml).root if Punchblock.jruby?
44
47
  ns = (node.namespace.href if node.namespace)
45
48
  klass = class_from_registration(node.element_name, ns)
46
49
  if klass && klass != self
@@ -60,6 +63,7 @@ module Punchblock
60
63
  # not provided one will be created
61
64
  # @return a new object with the registered name and namespace
62
65
  def self.new(name = registered_name, doc = nil)
66
+ raise InvalidNodeError, "Trying to create a new #{self} with no name" unless name
63
67
  super name, doc, registered_ns
64
68
  end
65
69
 
@@ -19,6 +19,8 @@ module Punchblock
19
19
  REDIRECT_EXTENSION = '1'
20
20
  REDIRECT_PRIORITY = '1'
21
21
 
22
+ trap_exit :actor_died
23
+
22
24
  def initialize(ami_client, connection, media_engine = nil)
23
25
  pb_logger.debug "Starting up..."
24
26
  @ami_client, @connection, @media_engine = ami_client, connection, media_engine
@@ -31,6 +33,11 @@ module Punchblock
31
33
  @calls[call.id] ||= call
32
34
  end
33
35
 
36
+ def deregister_call(call)
37
+ @channel_to_call_id.delete call.channel
38
+ @calls.delete call.id
39
+ end
40
+
34
41
  def call_with_id(call_id)
35
42
  @calls[call_id]
36
43
  end
@@ -54,24 +61,28 @@ module Punchblock
54
61
  end
55
62
 
56
63
  def handle_ami_event(event)
57
- return unless event.is_a? RubyAMI::Event
58
-
59
- if event.name.downcase == "fullybooted"
60
- pb_logger.trace "Counting FullyBooted event"
61
- @fully_booted_count += 1
62
- if @fully_booted_count >= 2
63
- handle_pb_event Connection::Connected.new
64
- @fully_booted_count = 0
65
- run_at_fully_booted
64
+ exclusive do
65
+ return unless event.is_a? RubyAMI::Event
66
+
67
+ if event.name.downcase == "fullybooted"
68
+ pb_logger.trace "Counting FullyBooted event"
69
+ @fully_booted_count += 1
70
+ if @fully_booted_count >= 2
71
+ handle_pb_event Connection::Connected.new
72
+ @fully_booted_count = 0
73
+ run_at_fully_booted
74
+ end
75
+ return
66
76
  end
67
- return
68
- end
69
77
 
70
- handle_varset_ami_event event
78
+ handle_varset_ami_event event
71
79
 
72
- ami_dispatch_to_or_create_call event
80
+ ami_dispatch_to_or_create_call event
73
81
 
74
- handle_pb_event Event::Asterisk::AMI::Event.new(:name => event.name, :attributes => event.headers)
82
+ unless ami_event_known_call?(event)
83
+ handle_pb_event Event::Asterisk::AMI::Event.new(:name => event.name, :attributes => event.headers)
84
+ end
85
+ end
75
86
  end
76
87
 
77
88
  def handle_pb_event(event)
@@ -98,7 +109,7 @@ module Punchblock
98
109
  if call = call_with_id(command.target_call_id)
99
110
  call.execute_command! command
100
111
  else
101
- command.response = ProtocolError.new.setup 'call-not-found', "Could not find a call with ID #{command.target_call_id}", command.target_call_id
112
+ command.response = ProtocolError.new.setup :item_not_found, "Could not find a call with ID #{command.target_call_id}", command.target_call_id
102
113
  end
103
114
  end
104
115
 
@@ -106,7 +117,7 @@ module Punchblock
106
117
  if (component = component_with_id(command.component_id))
107
118
  component.execute_command! command
108
119
  else
109
- command.response = ProtocolError.new.setup 'component-not-found', "Could not find a component with ID #{command.component_id}", command.target_call_id, command.component_id
120
+ 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
110
121
  end
111
122
  end
112
123
 
@@ -117,7 +128,7 @@ module Punchblock
117
128
  register_component component
118
129
  component.execute!
119
130
  when Punchblock::Command::Dial
120
- call = Call.new command.to, current_actor
131
+ call = Call.new_link command.to, current_actor
121
132
  register_call call
122
133
  call.dial! command
123
134
  else
@@ -134,6 +145,29 @@ module Punchblock
134
145
  'Command' => "dialplan add extension #{REDIRECT_EXTENSION},#{REDIRECT_PRIORITY},AGI,agi:async into #{REDIRECT_CONTEXT}"
135
146
  })
136
147
  pb_logger.trace "Added extension extension #{REDIRECT_EXTENSION},#{REDIRECT_PRIORITY},AGI,agi:async into #{REDIRECT_CONTEXT}"
148
+ send_ami_action('Command', {
149
+ 'Command' => "dialplan show #{REDIRECT_CONTEXT}"
150
+ }) do |result|
151
+ if result.text_body =~ /failed/
152
+ pb_logger.error "Punchblock failed to add the #{REDIRECT_EXTENSION} extension to the #{REDIRECT_CONTEXT} context. Please add a [#{REDIRECT_CONTEXT}] entry to your dialplan."
153
+ end
154
+ end
155
+ end
156
+
157
+ def check_recording_directory
158
+ pb_logger.warning "Recordings directory #{Component::Record::RECORDING_BASE_PATH} does not exist. Recording might not work. This warning can be ignored if Adhearsion is running on a separate machine than Asterisk. See http://adhearsion.com/docs/call-controllers#recording" unless File.exists?(Component::Record::RECORDING_BASE_PATH)
159
+ end
160
+
161
+ def actor_died(actor, reason)
162
+ return unless reason
163
+ pb_logger.error "A linked actor (#{actor.inspect}) died due to #{reason.inspect}"
164
+ if id = @calls.key(actor)
165
+ pb_logger.info "Dead actor was a call we know about, with ID #{id}. Removing it from the registry..."
166
+ @calls.delete id
167
+ end_event = Punchblock::Event::End.new :target_call_id => id,
168
+ :reason => :error
169
+ handle_pb_event end_event
170
+ end
137
171
  end
138
172
 
139
173
  private
@@ -149,10 +183,8 @@ module Punchblock
149
183
  end
150
184
 
151
185
  def ami_dispatch_to_or_create_call(event)
152
- if (event['Channel'] && call_for_channel(event['Channel'])) ||
153
- (event['Channel1'] && call_for_channel(event['Channel1'])) ||
154
- (event['Channel2'] && call_for_channel(event['Channel2']))
155
- [event['Channel'], event['Channel1'], event['Channel2']].compact.each do |channel|
186
+ if ami_event_known_call?(event)
187
+ channels_for_ami_event(event).each do |channel|
156
188
  call = call_for_channel channel
157
189
  call.process_ami_event! event if call
158
190
  end
@@ -161,14 +193,25 @@ module Punchblock
161
193
  end
162
194
  end
163
195
 
196
+ def channels_for_ami_event(event)
197
+ [event['Channel'], event['Channel1'], event['Channel2']].compact
198
+ end
199
+
200
+ def ami_event_known_call?(event)
201
+ (event['Channel'] && call_for_channel(event['Channel'])) ||
202
+ (event['Channel1'] && call_for_channel(event['Channel1'])) ||
203
+ (event['Channel2'] && call_for_channel(event['Channel2']))
204
+ end
205
+
164
206
  def handle_async_agi_start_event(event)
165
- env = Call.parse_environment event['Env']
207
+ env = RubyAMI::AsyncAGIEnvironmentParser.new(event['Env']).to_hash
166
208
 
167
209
  return pb_logger.warn "Ignoring AsyncAGI Start event because it is for an 'h' extension" if env[:agi_extension] == 'h'
168
210
  return pb_logger.warn "Ignoring AsyncAGI Start event because it is for an 'Kill' type" if env[:agi_type] == 'Kill'
169
211
 
170
212
  pb_logger.trace "Handling AsyncAGI Start event by creating a new call"
171
213
  call = Call.new event['Channel'], current_actor, env
214
+ link call
172
215
  register_call call
173
216
  call.send_offer!
174
217
  end
@@ -1,13 +1,12 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'uri'
4
-
5
3
  module Punchblock
6
4
  module Translator
7
5
  class Asterisk
8
6
  class Call
9
7
  include HasGuardedHandlers
10
8
  include Celluloid
9
+ include DeadActorSafety
11
10
 
12
11
  attr_reader :id, :channel, :translator, :agi_env, :direction, :pending_joins
13
12
 
@@ -21,26 +20,16 @@ module Punchblock
21
20
  HANGUP_CAUSE_TO_END_REASON[22] = :reject
22
21
  HANGUP_CAUSE_TO_END_REASON[102] = :timeout
23
22
 
24
- class << self
25
- def parse_environment(agi_env)
26
- agi_env_as_array(agi_env).inject({}) do |accumulator, element|
27
- accumulator[element[0].to_sym] = element[1] || ''
28
- accumulator
29
- end
30
- end
31
-
32
- def agi_env_as_array(agi_env)
33
- URI::Parser.new.unescape(agi_env).encode.split("\n").map { |p| p.split ': ' }
34
- end
35
- end
23
+ trap_exit :actor_died
36
24
 
37
25
  def initialize(channel, translator, agi_env = nil)
38
26
  @channel, @translator = channel, translator
39
27
  @agi_env = agi_env || {}
40
- @id, @components = UUIDTools::UUID.random_create.to_s, {}
28
+ @id, @components = Punchblock.new_uuid, {}
41
29
  @answered = false
42
30
  @pending_joins = {}
43
31
  pb_logger.debug "Starting up call with channel #{channel}, id #{@id}"
32
+ @progress_sent = false
44
33
  end
45
34
 
46
35
  def register_component(component)
@@ -96,9 +85,11 @@ module Punchblock
96
85
  @answered
97
86
  end
98
87
 
99
- def answer_if_not_answered
100
- return if answered? || outbound?
101
- execute_command Command::Answer.new.tap { |a| a.request! }
88
+ def send_progress
89
+ return if answered? || outbound? || @progress_sent
90
+ pb_logger.debug "Sending Progress to start early media"
91
+ @progress_sent = true
92
+ send_agi_action "EXEC Progress"
102
93
  end
103
94
 
104
95
  def channel=(other)
@@ -107,19 +98,23 @@ module Punchblock
107
98
  end
108
99
 
109
100
  def process_ami_event(ami_event)
101
+ send_pb_event Event::Asterisk::AMI::Event.new(:name => ami_event.name, :attributes => ami_event.headers)
102
+
110
103
  case ami_event.name
111
104
  when 'Hangup'
112
105
  pb_logger.trace "Received a Hangup AMI event. Sending End event."
113
106
  @components.dup.each_pair do |id, component|
114
- component.call_ended if component.alive?
107
+ safe_from_dead_actors do
108
+ component.call_ended if component.alive?
109
+ end
115
110
  end
116
111
  send_end_event HANGUP_CAUSE_TO_END_REASON[ami_event['Cause'].to_i]
117
112
  when 'AsyncAGI'
118
113
  pb_logger.trace "Received an AsyncAGI event. Looking for matching AGICommand component."
119
114
  if component = component_with_id(ami_event['CommandID'])
120
- component.handle_ami_event! ami_event
115
+ component.handle_ami_event ami_event
121
116
  else
122
- pb_logger.warn "Could not find component for AMI event: #{ami_event.inspect}"
117
+ pb_logger.trace "Could not find component for AMI event: #{ami_event.inspect}"
123
118
  end
124
119
  when 'Newstate'
125
120
  pb_logger.trace "Received a Newstate AMI event with state #{ami_event['ChannelState']}: #{ami_event['ChannelStateDesc']}"
@@ -130,6 +125,11 @@ module Punchblock
130
125
  @answered = true
131
126
  send_pb_event Event::Answered.new
132
127
  end
128
+ when 'OriginateResponse'
129
+ if ami_event['Response'] == 'Failure' && ami_event['Uniqueid'] == '<null>'
130
+ pb_logger.info "Outbound call could not be established!"
131
+ send_end_event :error
132
+ end
133
133
  when 'BridgeExec'
134
134
  if join_command = pending_joins[ami_event['Channel2']]
135
135
  join_command.response = true
@@ -165,9 +165,9 @@ module Punchblock
165
165
  pb_logger.debug "Executing command: #{command.inspect}"
166
166
  if command.component_id
167
167
  if component = component_with_id(command.component_id)
168
- component.execute_command! command
168
+ component.execute_command command
169
169
  else
170
- command.response = ProtocolError.new.setup 'component-not-found', "Could not find a component with ID #{command.component_id} for call #{id}", id, command.component_id
170
+ command.response = ProtocolError.new.setup :item_not_found, "Could not find a component with ID #{command.component_id} for call #{id}", id, command.component_id
171
171
  end
172
172
  end
173
173
  case command
@@ -182,7 +182,7 @@ module Punchblock
182
182
  end
183
183
  end
184
184
  when Command::Answer
185
- send_agi_action 'EXEC ANSWER' do |response|
185
+ send_agi_action 'ANSWER' do |response|
186
186
  command.response = true
187
187
  end
188
188
  when Command::Hangup
@@ -195,7 +195,14 @@ module Punchblock
195
195
  send_agi_action 'EXEC Bridge', other_call.channel
196
196
  when Command::Unjoin
197
197
  other_call = translator.call_with_id command.call_id
198
- redirect_back other_call
198
+ redirect_back other_call do |response|
199
+ case response
200
+ when RubyAMI::Error
201
+ command.response = ProtocolError.new.setup 'error', response.message
202
+ else
203
+ command.response = true
204
+ end
205
+ end
199
206
  when Command::Reject
200
207
  rejection = case command.reason
201
208
  when :busy
@@ -238,7 +245,7 @@ module Punchblock
238
245
  (name.is_a?(RubyAMI::Action) ? name : RubyAMI::Action.new(name, headers, &block)).tap do |action|
239
246
  @current_ami_action = action
240
247
  pb_logger.trace "Sending AMI action #{action.inspect}"
241
- translator.send_ami_action! action
248
+ translator.send_ami_action action
242
249
  end
243
250
  end
244
251
 
@@ -246,7 +253,7 @@ module Punchblock
246
253
  "#{self.class}: #{id}"
247
254
  end
248
255
 
249
- def redirect_back(other_call = nil)
256
+ def redirect_back(other_call = nil, &block)
250
257
  redirect_options = {
251
258
  'Channel' => channel,
252
259
  'Exten' => Asterisk::REDIRECT_EXTENSION,
@@ -259,18 +266,30 @@ module Punchblock
259
266
  'ExtraPriority' => Asterisk::REDIRECT_PRIORITY,
260
267
  'ExtraContext' => Asterisk::REDIRECT_CONTEXT
261
268
  }) if other_call
262
- send_ami_action 'Redirect', redirect_options
269
+ send_ami_action 'Redirect', redirect_options, &block
270
+ end
271
+
272
+ def actor_died(actor, reason)
273
+ return unless reason
274
+ pb_logger.error "A linked actor (#{actor.inspect}) died due to #{reason.inspect}"
275
+ if id = @components.key(actor)
276
+ pb_logger.info "Dead actor was a component we know about, with ID #{id}. Removing it from the registry..."
277
+ @components.delete id
278
+ complete_event = Punchblock::Event::Complete.new :component_id => id, :reason => Punchblock::Event::Complete::Error.new
279
+ send_pb_event complete_event
280
+ end
263
281
  end
264
282
 
265
283
  private
266
284
 
267
285
  def send_end_event(reason)
268
286
  send_pb_event Event::End.new(:reason => reason)
287
+ translator.deregister_call current_actor
269
288
  after(5) { shutdown }
270
289
  end
271
290
 
272
291
  def execute_component(type, command, options = {})
273
- type.new(command, current_actor).tap do |component|
292
+ type.new_link(command, current_actor).tap do |component|
274
293
  register_component component
275
294
  component.internal = true if options[:internal]
276
295
  component.execute!
@@ -280,7 +299,7 @@ module Punchblock
280
299
  def send_pb_event(event)
281
300
  event.target_call_id = id
282
301
  pb_logger.trace "Sending Punchblock event: #{event.inspect}"
283
- translator.handle_pb_event! event
302
+ translator.handle_pb_event event
284
303
  end
285
304
 
286
305
  def offer_event