# encoding: utf-8 require 'punchblock/translator/asterisk/ami_error_converter' module Punchblock module Translator class Asterisk class Call include HasGuardedHandlers include DeadActorSafety InvalidCommandError = Class.new Punchblock::Error OUTBOUND_CHANNEL_MATCH = /.* <(?.*)>/.freeze attr_reader :id, :channel, :translator, :agi_env, :direction, :pending_joins HANGUP_CAUSE_TO_END_REASON = Hash.new { :error } HANGUP_CAUSE_TO_END_REASON[0] = :hungup HANGUP_CAUSE_TO_END_REASON[16] = :hungup HANGUP_CAUSE_TO_END_REASON[17] = :busy HANGUP_CAUSE_TO_END_REASON[18] = :timeout HANGUP_CAUSE_TO_END_REASON[19] = :reject HANGUP_CAUSE_TO_END_REASON[21] = :reject HANGUP_CAUSE_TO_END_REASON[22] = :reject HANGUP_CAUSE_TO_END_REASON[102] = :timeout def initialize(channel, translator, ami_client, connection, agi_env = nil, id = nil) @channel, @translator, @ami_client, @connection = channel, translator, ami_client, connection @agi_env = agi_env || {} @id = id || Punchblock.new_uuid @components = {} @answered = false @pending_joins = {} @progress_sent = false @block_commands = false @channel_variables = {} @hangup_cause = nil 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 send_offer @direction = :inbound send_pb_event offer_event end def channel_var(variable) @channel_variables[variable] || fetch_channel_var(variable) end def to_s "#<#{self.class}:#{id} Channel: #{channel.inspect}>" end alias :inspect :to_s def dial(dial_command) @direction = :outbound channel = dial_command.to || '' channel.match(OUTBOUND_CHANNEL_MATCH) { |m| channel = m[:channel] } params = { :async => true, :context => REDIRECT_CONTEXT, :exten => REDIRECT_EXTENSION, :priority => REDIRECT_PRIORITY, :channel => channel, :callerid => dial_command.from } params[:variable] = variable_for_headers dial_command.headers params[:timeout] = dial_command.timeout unless dial_command.timeout.nil? originate_action = Punchblock::Component::Asterisk::AMI::Action.new :name => 'Originate', :params => params originate_action.request! translator.async.execute_global_command originate_action dial_command.response = Ref.new uri: id end def outbound? direction == :outbound end def inbound? direction == :inbound end def answered? @answered end def send_progress return if answered? || outbound? || @progress_sent @progress_sent = true execute_agi_command "EXEC Progress" end def channel=(other) @channel = other end def process_ami_event(ami_event) if Asterisk.event_passes_filter?(ami_event) send_pb_event Event::Asterisk::AMI::Event.new(name: ami_event.name, headers: ami_event.headers) end case ami_event.name when 'Hangup' handle_hangup_event ami_event['Cause'].to_i, ami_event.best_time when 'AsyncAGI' component_for_command_id_handle ami_event if @answered == false && ami_event['SubEvent'] == 'Start' @answered = true send_pb_event Event::Answered.new(timestamp: ami_event.best_time) end when 'AsyncAGIStart' component_for_command_id_handle ami_event if @answered == false @answered = true send_pb_event Event::Answered.new(timestamp: ami_event.best_time) end when 'AsyncAGIExec', 'AsyncAGIEnd' component_for_command_id_handle ami_event when 'Newstate' case ami_event['ChannelState'] when '5' send_pb_event Event::Ringing.new(timestamp: ami_event.best_time) end when 'BridgeEnter' if other_call_channel = translator.bridges.delete(ami_event['BridgeUniqueid']) if other_call = translator.call_for_channel(other_call_channel) join_command = @pending_joins.delete other_call_channel join_command ||= other_call.pending_joins.delete channel join_command.response = true if join_command event = Event::Joined.new call_uri: other_call.id, timestamp: ami_event.best_time send_pb_event event other_call_event = Event::Joined.new call_uri: id, timestamp: ami_event.best_time other_call_event.target_call_id = other_call.id translator.handle_pb_event other_call_event end else translator.bridges[ami_event['BridgeUniqueid']] = ami_event['Channel'] end when 'BridgeLeave' if other_call_channel = translator.bridges.delete(ami_event['BridgeUniqueid'] + '_leave') if other_call = translator.call_for_channel(other_call_channel) event = Event::Unjoined.new call_uri: other_call.id, timestamp: ami_event.best_time send_pb_event event other_call_event = Event::Unjoined.new call_uri: id, timestamp: ami_event.best_time other_call_event.target_call_id = other_call.id translator.handle_pb_event other_call_event end else translator.bridges[ami_event['BridgeUniqueid'] + '_leave'] = ami_event['Channel'] end when 'OriginateResponse' if ami_event['Response'] == 'Failure' && ami_event['Uniqueid'] == '' send_end_event :error, nil, ami_event.best_time end when 'BridgeExec' join_command = @pending_joins.delete ami_event['Channel1'] join_command ||= @pending_joins.delete ami_event['Channel2'] join_command.response = true if join_command when 'Bridge' other_call_channel = ([ami_event['Channel1'], ami_event['Channel2']] - [channel]).first if other_call = translator.call_for_channel(other_call_channel) event = case ami_event['Bridgestate'] when 'Link' Event::Joined.new call_uri: other_call.id, timestamp: ami_event.best_time when 'Unlink' Event::Unjoined.new call_uri: other_call.id, timestamp: ami_event.best_time end send_pb_event event end when 'Unlink' other_call_channel = ([ami_event['Channel1'], ami_event['Channel2']] - [channel]).first if other_call = translator.call_for_channel(other_call_channel) send_pb_event Event::Unjoined.new(call_uri: other_call.id, timestamp: ami_event.best_time) end when 'VarSet' @channel_variables[ami_event['Variable']] = ami_event['Value'] end trigger_handler :ami, ami_event end def send_message(body) execute_agi_command 'EXEC SendText', body rescue end def execute_command(command) if @block_commands command.response = ProtocolError.new.setup :item_not_found, "Could not find a call with ID #{id}", id return end if command.component_id 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} for call #{id}", id, command.component_id end end case command when Command::Accept if outbound? command.response = true else execute_agi_command 'EXEC RINGING' command.response = true end when Command::Answer execute_agi_command 'ANSWER' @answered = true command.response = true when Command::Hangup send_hangup_command @hangup_cause = :hangup_command command.response = true when Command::Join other_call = translator.call_with_id command.call_uri if other_call @pending_joins[other_call.channel] = command execute_agi_command 'EXEC Bridge', "#{other_call.channel},F(#{REDIRECT_CONTEXT},#{REDIRECT_EXTENSION},#{REDIRECT_PRIORITY})" else command.response = ProtocolError.new.setup :service_unavailable, "Could not find join party with address #{command.call_uri}", id end when Command::Unjoin other_call = translator.call_with_id command.call_uri redirect_back other_call command.response = true when Command::Reject case command.reason when :busy execute_agi_command 'EXEC Busy' when :decline send_hangup_command 21 when :error execute_agi_command 'EXEC Congestion' else execute_agi_command 'EXEC Congestion' end command.response = true when Command::Redirect execute_agi_command 'EXEC Transfer', command.to status = channel_var 'TRANSFERSTATUS' command.response = case status when 'SUCCESS' true else ProtocolError.new.setup 'error', "TRANSFERSTATUS was #{status}", id end when Punchblock::Component::Asterisk::AGI::Command execute_component Component::Asterisk::AGICommand, command when Punchblock::Component::Output execute_component Component::Output, command when Punchblock::Component::Input execute_component Component::Input, command when Punchblock::Component::Prompt component_class = case command.input.recognizer when 'unimrcp' case command.output.renderer when 'unimrcp' Component::MRCPPrompt when 'asterisk' Component::MRCPNativePrompt else raise InvalidCommandError, 'Invalid recognizer/renderer combination' end else Component::ComposedPrompt end execute_component component_class, command when Punchblock::Component::Record execute_component Component::Record, command else command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command for call #{id}", id end rescue InvalidCommandError => e command.response = ProtocolError.new.setup :invalid_command, e.message, id rescue ChannelGoneError command.response = ProtocolError.new.setup :item_not_found, "Could not find a call with ID #{id}", id rescue RubyAMI::Error => e command.response = ProtocolError.new.setup 'error', e.message, id rescue Celluloid::DeadActorError 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 end # # @return [Hash] AGI result # # @raises RubyAMI::Error, ChannelGoneError def execute_agi_command(command, *params) agi = AGICommand.new Punchblock.new_uuid, channel, command, *params response = Celluloid::Future.new register_tmp_handler :ami, [{name: 'AsyncAGI', [:[], 'SubEvent'] => 'Exec'}, {name: 'AsyncAGIExec'}], [{[:[], 'CommandID'] => agi.id}, {[:[], 'CommandId'] => agi.id}] do |event| response.signal Celluloid::SuccessResponse.new(nil, event) end agi.execute @ami_client event = response.value return unless event agi.parse_result event end def logger_id "#{self.class}: #{id}" end def redirect_back(other_call = nil) redirect_options = { 'Channel' => channel, 'Exten' => Asterisk::REDIRECT_EXTENSION, 'Priority' => Asterisk::REDIRECT_PRIORITY, 'Context' => Asterisk::REDIRECT_CONTEXT } redirect_options.merge!({ 'ExtraChannel' => other_call.channel, 'ExtraExten' => Asterisk::REDIRECT_EXTENSION, 'ExtraPriority' => Asterisk::REDIRECT_PRIORITY, 'ExtraContext' => Asterisk::REDIRECT_CONTEXT }) if other_call send_ami_action 'Redirect', redirect_options end def handle_hangup_event(code = nil, timestamp = nil) code ||= 16 reason = @hangup_cause || HANGUP_CAUSE_TO_END_REASON[code] @block_commands = true @components.each_pair do |id, component| component.call_ended end send_end_event reason, code, timestamp end def after(*args, &block) translator.after(*args, &block) end private def fetch_channel_var(variable) result = @ami_client.send_action 'GetVar', 'Channel' => channel, 'Variable' => variable result['Value'] == '(null)' ? nil : result['Value'] end def send_hangup_command(cause_code = 16) send_ami_action 'Hangup', 'Channel' => channel, 'Cause' => cause_code end def send_ami_action(name, headers = {}) AMIErrorConverter.convert { @ami_client.send_action name, headers } end def send_end_event(reason, code = nil, timestamp = nil) end_event = Event::End.new(reason: reason, platform_code: code, timestamp: timestamp) send_pb_event end_event translator.deregister_call id, channel end def execute_component(type, command, options = {}) type.new(command, self).tap do |component| register_component component component.execute end end def send_pb_event(event) event.target_call_id = id translator.handle_pb_event event end def offer_event Event::Offer.new :to => agi_env.values_at(:agi_dnid, :agi_extension).detect { |e| e && e != 'unknown' }, :from => "#{agi_env[:agi_calleridname]} <#{[agi_env[:agi_type], agi_env[:agi_callerid]].join('/')}>", :headers => sip_headers end def sip_headers agi_env.to_a.inject({}) do |accumulator, element| accumulator['X-' + element[0].to_s] = element[1] || '' accumulator end end def component_for_command_id_handle(ami_event) if component = component_with_id(ami_event['CommandID'] || ami_event['CommandId']) component.handle_ami_event ami_event end end def variable_for_headers(headers) variables = { :punchblock_call_id => id } header_counter = 51 headers.each do |name, value| variables["SIPADDHEADER#{header_counter}"] = "\"#{name}: #{value}\"" header_counter += 1 end variables.inject([]) do |a, (k, v)| a << "#{k}=#{v}" end.join(',') end end end end end