lib/adhearsion/translator/asterisk.rb
# encoding: utf-8
require 'celluloid'
require 'ruby_ami'
require 'adhearsion/translator/asterisk/agi_command'
require 'adhearsion/translator/asterisk/call'
require 'adhearsion/translator/asterisk/channel'
require 'adhearsion/translator/asterisk/component'
module Adhearsion
module Translator
class Asterisk
include Celluloid
# Indicates that a command was executed against a channel which no longer exists
ChannelGoneError = Class.new Error
attr_reader :ami_client, :connection, :calls, :bridges
REDIRECT_CONTEXT = 'adhearsion-redirect'
REDIRECT_EXTENSION = '1'
REDIRECT_PRIORITY = '1'
EVENTS_ALLOWED_BRIDGED = %w{AGIExec AsyncAGI}
trap_exit :actor_died
# 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).
#
# @param [#[<RubyAMI::Event>]] filter
#
# @example A lambda
# Translator::Asterisk.event_filter = ->(event) { event.name == 'AsyncAGI' }
#
def self.event_filter=(filter)
@event_filter = filter
end
def self.event_passes_filter?(event)
@event_filter ? !!@event_filter[event] : true
end
event_filter = nil
def initialize(ami_client, connection)
@ami_client, @connection = ami_client, connection
@calls, @components, @channel_to_call_id, @bridges = {}, {}, {}, {}
end
def register_call(call)
@channel_to_call_id[call.channel] = call.id
@calls[call.id] ||= call
end
def deregister_call(id, channel)
@channel_to_call_id.delete channel
@calls.delete id
end
def call_with_id(call_id)
@calls[call_id]
end
def call_for_channel(channel)
call_with_id @channel_to_call_id[Channel.new(channel).name]
end
def register_component(component)
@components[component.id] ||= component
end
def deregister_component(id)
@components.delete id
end
def component_with_id(component_id)
@components[component_id]
end
def handle_ami_event(event)
return unless event.is_a? RubyAMI::Event
if event.name == 'FullyBooted'
handle_pb_event Adhearsion::Rayo::Connection::Connected.new
run_at_fully_booted
return
end
handle_varset_ami_event event
ami_dispatch_to_or_create_call event
if !ami_event_known_call?(event) && self.class.event_passes_filter?(event)
handle_pb_event Adhearsion::Event::Asterisk::AMI.new(name: event.name, headers: event.headers)
end
end
def handle_pb_event(event)
connection.handle_event event
end
def send_message(call_id, domain, body, options = {})
call = call_with_id call_id
call.send_message body if call
end
def execute_command(command, options = {})
command.request!
command.target_call_id ||= options[:call_id]
command.component_id ||= options[:component_id]
if command.target_call_id
execute_call_command command
elsif command.component_id
execute_component_command command
else
execute_global_command command
end
end
def execute_call_command(command)
if call = call_with_id(command.target_call_id)
begin
call.execute_command command
rescue => e
Adhearsion::Events.trigger :exception, [e, logger]
deregister_call call.id, call.channel
command.response = Adhearsion::ProtocolError.new.setup :error, "Unknown error executing command on call #{command.target_call_id}", command.target_call_id
end
else
command.response = Adhearsion::ProtocolError.new.setup :item_not_found, "Could not find a call with ID #{command.target_call_id}", command.target_call_id
end
end
def execute_component_command(command)
if (component = component_with_id(command.component_id))
component.execute_command command
else
command.response = Adhearsion::ProtocolError.new.setup :item_not_found, "Could not find a component with ID #{command.component_id}", command.target_call_id, command.component_id
end
end
def execute_global_command(command)
case command
when Adhearsion::Rayo::Component::Asterisk::AMI::Action
component = Component::Asterisk::AMIAction.new command, current_actor, ami_client
register_component component
component.execute
when Adhearsion::Rayo::Command::Dial
if call = call_with_id(command.uri)
command.response = Adhearsion::ProtocolError.new.setup(:conflict, 'Call ID already in use')
else
call = Call.new command.to, self, ami_client, connection, nil, command.uri
register_call call
call.dial command
end
else
command.response = Adhearsion::ProtocolError.new.setup 'command-not-acceptable', "Did not understand command"
end
end
def run_at_fully_booted
send_ami_action 'Command', 'Command' => "dialplan add extension #{REDIRECT_EXTENSION},#{REDIRECT_PRIORITY},AGI,agi:async into #{REDIRECT_CONTEXT}"
result = send_ami_action 'Command', 'Command' => "dialplan show #{REDIRECT_CONTEXT}"
if result.text_body =~ /failed/
logger.error "Adhearsion failed to add the #{REDIRECT_EXTENSION} extension to the #{REDIRECT_CONTEXT} context. Please add a [#{REDIRECT_CONTEXT}] entry to your dialplan."
end
check_recording_directory
end
def check_recording_directory
logger.warn "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)
end
def actor_died(actor, reason)
return unless reason
if id = @calls.key(actor)
@calls.delete id
end_event = Adhearsion::Event::End.new :target_call_id => id,
:reason => :error
handle_pb_event end_event
end
end
private
def send_ami_action(name, headers = {})
ami_client.send_action name, headers
end
def handle_varset_ami_event(event)
return unless event.name == 'VarSet' && event['Variable'] == 'adhearsion_call_id' && (call = call_with_id event['Value'])
@channel_to_call_id.delete call.channel
call.channel = event['Channel']
register_call call
end
def ami_dispatch_to_or_create_call(event)
calls_for_event = channels_for_ami_event(event).inject({}) do |h, channel|
call = call_for_channel channel
h[channel] = call if call
h
end
if !calls_for_event.empty?
calls_for_event.each_pair do |channel, call|
next if channel.bridged? && !EVENTS_ALLOWED_BRIDGED.include?(event.name)
call.process_ami_event event
end
elsif event.name == "AsyncAGIStart" || (event.name == "AsyncAGI" && event['SubEvent'] == "Start")
handle_async_agi_start_event event
end
end
def channels_for_ami_event(event)
[event['Channel'], event['Channel1'], event['Channel2']].compact.map { |channel| Channel.new(channel) }
end
def ami_event_known_call?(event)
(event['Channel'] && call_for_channel(event['Channel'])) ||
(event['Channel1'] && call_for_channel(event['Channel1'])) ||
(event['Channel2'] && call_for_channel(event['Channel2']))
end
def handle_async_agi_start_event(event)
env = RubyAMI::AsyncAGIEnvironmentParser.new(event['Env']).to_hash
return if env[:agi_extension] == 'h' || env[:agi_type] == 'Kill'
call = Call.new event['Channel'], self, ami_client, connection, env
register_call call
call.send_offer
end
end
end
end