punchblock 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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