lib/adhearsion/call_controller.rb
# encoding: utf-8
require 'countdownlatch'
%w(
dial
input
output
record
).each { |r| require "adhearsion/call_controller/#{r}" }
module Adhearsion
class CallController
include Dial
include Input
include Output
include Record
class_attribute :callbacks
self.callbacks = {:before => [], :after => [], :on_error => []}
self.callbacks.keys.each do |name|
class_eval <<-STOP
def self.#{name}(method_name = nil, &block)
callback = if method_name
lambda { send method_name }
elsif block
block
end
self.callbacks = self.callbacks.dup.tap { |cb| cb[:#{name}] += Array(callback) }
end
STOP
end
class << self
#
# Execute a call controller, allowing passing control to another controller
#
# @param [CallController] controller
#
def exec(controller)
controller.exec
end
#
# Include another module into all CallController classes
#
def mixin(mod)
include mod
end
def before_call(*args, &block)
Adhearsion.deprecated :before
before(*args, &block)
end
def after_call(*args, &block)
Adhearsion.deprecated :after
after(*args, &block)
end
end
# @return [Call] The call object on which the controller is executing
attr_reader :call
# @return [Hash] The controller's metadata provided at invocation
attr_reader :metadata
# @private
attr_reader :block
delegate :[], :[]=, :to => :@metadata
delegate :variables, :send_message, :to => :call
#
# Create a new instance
#
# @param [Call] call the call to operate the controller on
# @param [Hash] metadata generic key-value storage applicable to the controller
# @param block to execute on the call
#
def initialize(call, metadata = nil, &block)
@call, @metadata, @block = call, metadata || {}, block
@block_context = eval "self", @block.binding if @block
@active_components = []
end
def method_missing(method_name, *args, &block)
if @block_context
@block_context.send method_name, *args, &block
else
super
end
end
#
# Execute the controller, allowing passing control to another controller
#
def exec(controller = self)
new_controller = catch :pass_controller do
controller.execute!
nil
end
exec new_controller if new_controller
end
def bg_exec(completion_callback = nil)
Celluloid::ThreadHandle.new(Celluloid.actor_system) do
catching_standard_errors do
exec_with_callback completion_callback
end
end
end
def exec_with_callback(completion_callback = nil)
exec
ensure
completion_callback.call call if completion_callback
end
# @private
def execute!(*options)
call.async.register_controller self
execute_callbacks :before
run
rescue Call::Hangup, Call::ExpiredError
rescue SyntaxError, StandardError => e
Events.trigger :exception, [e, logger]
on_error e
raise
ensure
after
logger.debug "Finished executing controller #{self.class}"
end
#
# Invoke the block supplied when creating the controller
#
def run
instance_exec(&block) if block
end
#
# Invoke another controller class within this controller, returning to this context on completion.
#
# @param [Class] controller_class The class of controller to execute
# @param [Hash] metadata generic key-value storage applicable to the controller
# @return The return value of the controller's run method
#
def invoke(controller_class, metadata = nil)
controller = controller_class.new call, metadata
controller.run
end
#
# Cease execution of this controller, and pass to another.
#
# @param [Class] controller_class The class of controller to pass to
# @param [Hash] metadata generic key-value storage applicable to the controller
#
def pass(controller_class, metadata = nil)
throw :pass_controller, controller_class.new(call, metadata)
end
#
# Stop execution of all the components currently running in the controller.
#
def stop_all_components
logger.info "Stopping all controller components"
@active_components.each do |component|
begin
component.stop!
rescue Adhearsion::Rayo::Component::InvalidActionError
end
end
end
#
# Cease execution of this controller, including any components it is executing, and pass to another.
#
# @param [Class] controller_class The class of controller to pass to
# @param [Hash] metadata generic key-value storage applicable to the controller
#
def hard_pass(controller_class, metadata = nil)
logger.info "Hard passing with active components #{@active_components.inspect}"
stop_all_components
pass controller_class, metadata
end
# @private
def execute_callbacks(type)
self.class.callbacks[type].each do |callback|
catching_standard_errors do
instance_exec(&callback)
end
end
end
# @private
def after
@after ||= execute_callbacks :after
end
# @private
def on_error(e)
@error = e
@on_error ||= execute_callbacks :on_error
end
# @private
def write_and_await_response(command)
block_until_resumed
call.write_and_await_response command
if command.is_a?(Adhearsion::Rayo::Component::ComponentNode)
command.register_event_handler Adhearsion::Event::Complete do |event|
@active_components.delete command
throw :pass
end
@active_components << command
end
end
# @private
def execute_component_and_await_completion(component)
write_and_await_response component
yield component if block_given?
complete_event = component.complete_event
raise Adhearsion::Error, [complete_event.reason.details, component.inspect].join(": ") if complete_event.reason.is_a? Adhearsion::Event::Complete::Error
component
end
#
# Answer the call
#
# @see Call#answer
#
def answer(*args)
block_until_resumed
call.answer(*args)
end
#
# Hangup the call, and execute after callbacks
#
# @param [Hash] headers
#
def hangup(headers = nil)
block_until_resumed
call.hangup headers
raise Call::Hangup
end
#
# Reject the call
#
# @see Call#reject
#
def reject(*args)
block_until_resumed
call.reject(*args)
raise Call::Hangup
end
#
# Redirect the call to some other target
#
# @see Call#redirect
#
def redirect(*args)
block_until_resumed
call.redirect(*args)
raise Call::Hangup
end
#
# Mute the call
#
# @see Call#mute
#
def mute(*args)
block_until_resumed
call.mute(*args)
end
#
# Unmute the call
#
# @see Call#unmute
#
def unmute(*args)
block_until_resumed
call.unmute(*args)
end
#
# Join the call to another call or a mixer, and block until the call is unjoined (by hangup or otherwise).
#
# @param [Object] target See Call#join for details
# @param [Hash] options
# @option options [Boolean] :async Return immediately, without waiting for the calls to unjoin. Defaults to false.
#
# @see Call#join
#
def join(target, options = {})
block_until_resumed
async = (target.is_a?(Hash) ? target : options).delete :async
join = call.join target, options
waiter = async ? join[:joined_condition] : join[:unjoined_condition]
waiter.wait
end
alias :safely :catching_standard_errors
# @private
def block_until_resumed
instance_variable_defined?(:@pause_latch) && @pause_latch.wait
end
# @private
def pause!
@pause_latch = CountDownLatch.new 1
end
# @private
def resume!
return unless @pause_latch
@pause_latch.countdown!
@pause_latch = nil
end
# @private
def inspect
"#<#{self.class} call=#{call.alive? ? call.id : ''}, metadata=#{metadata.inspect}>"
end
def eql?(other)
other.instance_of?(self.class) && call == other.call && metadata == other.metadata
end
alias :== :eql?
def logger
call.logger
rescue Celluloid::DeadActorError
super
end
end#class
end