lib/punchblock/translator/asterisk.rb
# encoding: utf-8
require 'celluloid'
require 'ruby_ami'
module Punchblock
module Translator
class Asterisk
include Celluloid
# Indicates that a command was executed against a channel which no longer exists
ChannelGoneError = Class.new Punchblock::Error
extend ActiveSupport::Autoload
autoload :AGICommand
autoload :Call
autoload :Channel
autoload :Component
attr_reader :ami_client, :connection, :calls
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
# Punchblock::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
case event.name
when 'FullyBooted'
handle_pb_event Connection::Connected.new
run_at_fully_booted
return
when 'BridgeEnter'
if other_channel = @bridges.delete(event['BridgeUniqueid'])
if event['OtherCall'] = call_for_channel(other_channel)
join_command = event['OtherCall'].pending_joins.delete event['Channel']
call = call_for_channel(event['Channel'])
join_command ||= call.pending_joins.delete other_channel if call
join_command.response = true if join_command
end
else
@bridges[event['BridgeUniqueid']] = event['Channel']
end
when 'BridgeLeave'
if other_channel = @bridges.delete(event['BridgeUniqueid'] + '_leave')
event['OtherCall'] = call_for_channel(other_channel)
else
@bridges[event['BridgeUniqueid'] + '_leave'] = event['Channel']
end
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 Event::Asterisk::AMI::Event.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
deregister_call call.id, call.channel
end
else
command.response = 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 = 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 Punchblock::Component::Asterisk::AMI::Action
component = Component::Asterisk::AMIAction.new command, current_actor, ami_client
register_component component
component.execute
when Punchblock::Command::Dial
if call = call_with_id(command.uri)
command.response = ProtocolError.new.setup(:conflict, 'Call ID already in use')
else
call = Call.new command.to, current_actor, ami_client, connection, nil, command.uri
register_call call
call.dial command
end
else
command.response = 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/
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."
end
check_recording_directory
end
def check_recording_directory
pb_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 = Punchblock::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'] == 'punchblock_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'], current_actor, ami_client, connection, env
register_call call
call.send_offer
end
end
end
end