From 910a125517934d2b14d63979b58d421b11721476 Mon Sep 17 00:00:00 2001 From: Ben Langfeld Date: Wed, 8 Jan 2014 12:33:32 -0200 Subject: [PATCH 1/2] [CHANGE] Remove FreeSWITCH translator --- CHANGELOG.md | 1 + README.markdown | 1 - lib/punchblock.rb | 2 +- lib/punchblock/connection.rb | 1 - lib/punchblock/connection/freeswitch.rb | 53 - lib/punchblock/translator.rb | 1 - lib/punchblock/translator/freeswitch.rb | 164 --- lib/punchblock/translator/freeswitch/call.rb | 279 ----- .../translator/freeswitch/component.rb | 93 -- .../freeswitch/component/abstract_output.rb | 63 - .../freeswitch/component/flite_output.rb | 21 - .../translator/freeswitch/component/input.rb | 35 - .../translator/freeswitch/component/output.rb | 56 - .../translator/freeswitch/component/record.rb | 93 -- .../freeswitch/component/tts_output.rb | 29 - spec/punchblock/connection/freeswitch_spec.rb | 79 -- .../translator/freeswitch/call_spec.rb | 1019 ----------------- .../freeswitch/component/flite_output_spec.rb | 283 ----- .../freeswitch/component/input_spec.rb | 386 ------- .../freeswitch/component/output_spec.rb | 347 ------ .../freeswitch/component/record_spec.rb | 415 ------- .../freeswitch/component/tts_output_spec.rb | 302 ----- .../translator/freeswitch/component_spec.rb | 114 -- spec/punchblock/translator/freeswitch_spec.rb | 652 ----------- spec/punchblock_spec.rb | 10 - 25 files changed, 2 insertions(+), 4497 deletions(-) delete mode 100644 lib/punchblock/connection/freeswitch.rb delete mode 100644 lib/punchblock/translator/freeswitch.rb delete mode 100644 lib/punchblock/translator/freeswitch/call.rb delete mode 100644 lib/punchblock/translator/freeswitch/component.rb delete mode 100644 lib/punchblock/translator/freeswitch/component/abstract_output.rb delete mode 100644 lib/punchblock/translator/freeswitch/component/flite_output.rb delete mode 100644 lib/punchblock/translator/freeswitch/component/input.rb delete mode 100644 lib/punchblock/translator/freeswitch/component/output.rb delete mode 100644 lib/punchblock/translator/freeswitch/component/record.rb delete mode 100644 lib/punchblock/translator/freeswitch/component/tts_output.rb delete mode 100644 spec/punchblock/connection/freeswitch_spec.rb delete mode 100644 spec/punchblock/translator/freeswitch/call_spec.rb delete mode 100644 spec/punchblock/translator/freeswitch/component/flite_output_spec.rb delete mode 100644 spec/punchblock/translator/freeswitch/component/input_spec.rb delete mode 100644 spec/punchblock/translator/freeswitch/component/output_spec.rb delete mode 100644 spec/punchblock/translator/freeswitch/component/record_spec.rb delete mode 100644 spec/punchblock/translator/freeswitch/component/tts_output_spec.rb delete mode 100644 spec/punchblock/translator/freeswitch/component_spec.rb delete mode 100644 spec/punchblock/translator/freeswitch_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 8650847b..4d5988e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # [develop](https://github.com/adhearsion/punchblock) + * Change: Remove support for FreeSWITCH translator on Inbound EventSocket # [v2.7.0](https://github.com/adhearsion/punchblock/compare/v2.6.0...v2.7.0) - [2015-06-09](https://rubygems.org/gems/punchblock/versions/2.7.0) * Feature: Support for Asterisk 13 (AMI v2) diff --git a/README.markdown b/README.markdown index 08b941bf..1f9d47f5 100644 --- a/README.markdown +++ b/README.markdown @@ -21,7 +21,6 @@ The best available usage documentation available for Punchblock is by example, i * Rayo * Asterisk (AMI & AsyncAGI) -* FreeSWITCH (Inbound Event Socket) ## Links: * [Source](https://github.com/adhearsion/punchblock) diff --git a/lib/punchblock.rb b/lib/punchblock.rb index 660c433f..145edee7 100644 --- a/lib/punchblock.rb +++ b/lib/punchblock.rb @@ -45,7 +45,7 @@ def reset_logger # # Get a new Punchblock client with a connection attached # - # @param [Symbol] type the connection type (eg :XMPP, :asterisk, :freeswitch) + # @param [Symbol] type the connection type (eg :XMPP, :asterisk) # @param [Hash] options the options to pass to the connection (credentials, etc # # @return [Punchblock::Client] a punchblock client object diff --git a/lib/punchblock/connection.rb b/lib/punchblock/connection.rb index 4e15ba9c..d9669209 100644 --- a/lib/punchblock/connection.rb +++ b/lib/punchblock/connection.rb @@ -6,7 +6,6 @@ module Connection autoload :Asterisk autoload :Connected - autoload :Freeswitch autoload :GenericConnection autoload :XMPP end diff --git a/lib/punchblock/connection/freeswitch.rb b/lib/punchblock/connection/freeswitch.rb deleted file mode 100644 index c0016b80..00000000 --- a/lib/punchblock/connection/freeswitch.rb +++ /dev/null @@ -1,53 +0,0 @@ -# encoding: utf-8 - -require 'ruby_fs' - -module Punchblock - module Connection - class Freeswitch < GenericConnection - attr_reader :translator, :stream - attr_accessor :event_handler - - def initialize(options = {}) - @translator = Translator::Freeswitch.new self - @stream_options = options.values_at(:host, :port, :password) - @stream = new_fs_stream - super() - end - - def run - pb_logger.debug "Starting the RubyFS stream" - start_stream - raise DisconnectedError - end - - def stop - stream.shutdown - translator.terminate - end - - def write(command, options) - translator.async.execute_command command, options - end - - def handle_event(event) - event_handler.call event if event_handler.respond_to?(:call) - end - - private - - def new_fs_stream - RubyFS::Stream.new(*@stream_options, lambda { |e| translator.async.handle_es_event e }, event_mask) - end - - def event_mask - %w{CHANNEL_PARK CHANNEL_ANSWER CHANNEL_STATE CHANNEL_HANGUP CHANNEL_BRIDGE CHANNEL_UNBRIDGE CHANNEL_EXECUTE_COMPLETE DTMF RECORD_STOP} - end - - def start_stream - @stream = new_fs_stream unless @stream.alive? - @stream.run - end - end - end -end diff --git a/lib/punchblock/translator.rb b/lib/punchblock/translator.rb index 7fe0e3cb..443a1417 100644 --- a/lib/punchblock/translator.rb +++ b/lib/punchblock/translator.rb @@ -7,7 +7,6 @@ module Translator OptionError = Class.new Punchblock::Error autoload :Asterisk - autoload :Freeswitch autoload :DTMFRecognizer autoload :InputComponent diff --git a/lib/punchblock/translator/freeswitch.rb b/lib/punchblock/translator/freeswitch.rb deleted file mode 100644 index a0c43437..00000000 --- a/lib/punchblock/translator/freeswitch.rb +++ /dev/null @@ -1,164 +0,0 @@ -# encoding: utf-8 - -require 'celluloid' -require 'ruby_fs' - -module Punchblock - module Translator - class Freeswitch - include Celluloid - include HasGuardedHandlers - include DeadActorSafety - - extend ActorHasGuardedHandlers - execute_guarded_handlers_on_receiver - - extend ActiveSupport::Autoload - - autoload :Call - autoload :Component - - attr_reader :connection, :calls - - trap_exit :actor_died - - finalizer :finalize - - def initialize(connection) - @connection = connection - @calls, @components = {}, {} - setup_handlers - end - - def register_call(call) - @calls[call.id] ||= call - end - - def deregister_call(id) - @calls.delete id - end - - def call_with_id(call_id) - @calls[call_id] - end - - def register_component(component) - @components[component.id] ||= component - end - - def component_with_id(component_id) - @components[component_id] - end - - def setup_handlers - register_handler :es, RubyFS::Stream::Connected do - handle_pb_event Connection::Connected.new - throw :halt - end - - register_handler :es, RubyFS::Stream::Disconnected do - throw :halt - end - - register_handler :es, :event_name => 'CHANNEL_PARK' do |event| - throw :pass if es_event_known_call? event - call = Call.new event[:unique_id], current_actor, event.content.select { |k,v| k.to_s =~ /variable/ }, stream - link call - register_call call - call.async.send_offer - end - - register_handler :es, :event_name => ['CHANNEL_BRIDGE', 'CHANNEL_UNBRIDGE'], [:has_key?, :other_leg_unique_id] => true do |event| - call = call_with_id event[:other_leg_unique_id] - call.async.handle_es_event event if call - throw :pass - end - - register_handler :es, lambda { |event| es_event_known_call? event } do |event| - call = call_with_id event[:unique_id] - call.async.handle_es_event event - end - end - - def stream - connection.stream - end - - def finalize - @calls.values.each do |call| - safe_from_dead_actors do - call.terminate - end - end - end - - def handle_es_event(event) - trigger_handler :es, event - end - exclusive :handle_es_event - - def handle_pb_event(event) - connection.handle_event event - 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) - call.async.execute_command command - 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.async.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::Command::Dial - call = Call.new_link Punchblock.new_uuid, current_actor, nil, stream - register_call call - call.async.dial command - else - command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command" - end - end - - def actor_died(actor, reason) - return unless reason - pb_logger.error "A linked actor (#{actor.inspect}) died due to #{reason.inspect}" - 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 es_event_known_call?(event) - event[:unique_id] && call_with_id(event[:unique_id]) - end - end - end -end diff --git a/lib/punchblock/translator/freeswitch/call.rb b/lib/punchblock/translator/freeswitch/call.rb deleted file mode 100644 index 2e86f81d..00000000 --- a/lib/punchblock/translator/freeswitch/call.rb +++ /dev/null @@ -1,279 +0,0 @@ -# encoding: utf-8 - -module Punchblock - module Translator - class Freeswitch - class Call - include HasGuardedHandlers - include Celluloid - include DeadActorSafety - - extend ActorHasGuardedHandlers - execute_guarded_handlers_on_receiver - - HANGUP_CAUSE_TO_END_REASON = Hash.new :error - - HANGUP_CAUSE_TO_END_REASON['USER_BUSY'] = :busy - HANGUP_CAUSE_TO_END_REASON['MANAGER_REQUEST'] = :hangup_command - - %w{ - NORMAL_CLEARING ORIGINATOR_CANCEL SYSTEM_SHUTDOWN - BLIND_TRANSFER ATTENDED_TRANSFER PICKED_OFF NORMAL_UNSPECIFIED - }.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :hangup } - - %w{ - NO_USER_RESPONSE NO_ANSWER SUBSCRIBER_ABSENT ALLOTTED_TIMEOUT - MEDIA_TIMEOUT PROGRESS_TIMEOUT - }.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :timeout } - - %w{CALL_REJECTED NUMBER_CHANGED - REDIRECTION_TO_NEW_DESTINATION FACILITY_REJECTED NORMAL_CIRCUIT_CONGESTION - SWITCH_CONGESTION USER_NOT_REGISTERED FACILITY_NOT_SUBSCRIBED - OUTGOING_CALL_BARRED INCOMING_CALL_BARRED BEARERCAPABILITY_NOTAUTH - BEARERCAPABILITY_NOTAVAIL SERVICE_UNAVAILABLE BEARERCAPABILITY_NOTIMPL - CHAN_NOT_IMPLEMENTED FACILITY_NOT_IMPLEMENTED SERVICE_NOT_IMPLEMENTED - }.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :reject } - - REJECT_TO_HANGUP_REASON = Hash.new 'NORMAL_TEMPORARY_FAILURE' - REJECT_TO_HANGUP_REASON.merge! :busy => 'USER_BUSY', :decline => 'CALL_REJECTED' - - attr_reader :id, :translator, :es_env, :direction, :stream - - trap_exit :actor_died - - def initialize(id, translator, es_env = nil, stream = nil) - @id, @translator, @stream = id, translator, stream - @es_env = es_env || {} - @components = {} - @pending_joins, @pending_unjoins = {}, {} - @answered = false - setup_handlers - end - - def register_component(component) - @components[component.id] ||= component - end - - def component_with_id(component_id) - @components[component_id] - end - - def send_offer - @direction = :inbound - send_pb_event offer_event - end - - def to_s - "#<#{self.class}:#{id}>" - end - alias :inspect :to_s - - def setup_handlers - register_handler :es, :event_name => 'CHANNEL_ANSWER' do - @answered = true - send_pb_event Event::Answered.new - throw :pass - end - - register_handler :es, :event_name => 'CHANNEL_STATE', [:[], :channel_call_state] => 'RINGING' do - send_pb_event Event::Ringing.new - end - - register_handler :es, :event_name => 'CHANNEL_HANGUP' do |event| - @components.dup.each_pair do |id, component| - safe_from_dead_actors do - component.call_ended if component.alive? - end - end - send_end_event HANGUP_CAUSE_TO_END_REASON[event[:hangup_cause]] - end - - register_handler :es, :event_name => 'CHANNEL_BRIDGE' do |event| - command = @pending_joins[event[:other_leg_unique_id]] - command.response = true if command - - other_call_uri = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id] - send_pb_event Event::Joined.new(:call_uri => other_call_uri) - end - - register_handler :es, :event_name => 'CHANNEL_UNBRIDGE' do |event| - command = @pending_unjoins[event[:other_leg_unique_id]] - command.response = true if command - - other_call_uri = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id] - send_pb_event Event::Unjoined.new(:call_uri => other_call_uri) - end - - register_handler :es, [:has_key?, :scope_variable_punchblock_component_id] => true do |event| - if component = component_with_id(event[:scope_variable_punchblock_component_id]) - safe_from_dead_actors { component.handle_es_event event if component.alive? } - end - throw :pass - end - end - - def handle_es_event(event) - trigger_handler :es, event - end - - def application(*args) - stream.application id, *args - end - - def sendmsg(*args) - stream.sendmsg id, *args - end - - def uuid_foo(app, args = '') - stream.bgapi "uuid_#{app} #{id} #{args}" - end - - def dial(dial_command) - @direction = :outbound - - cid_number, cid_name = dial_command.from, nil - if dial_command.from - dial_command.from.match(/(?.*)<(?.*)>/) do |m| - cid_name = m[:cid_name].strip - cid_number = m[:cid_number] - end - end - - options = { - :return_ring_ready => true, - :origination_uuid => id - } - options[:origination_caller_id_number] = "'#{cid_number}'" if cid_number.present? - options[:origination_caller_id_name] = "'#{cid_name}'" if cid_name.present? - options[:originate_timeout] = dial_command.timeout/1000 if dial_command.timeout - dial_command.headers.each do |name, value| - options["sip_h_#{name}"] = "'#{value}'" - end - opts = options.inject([]) do |a, (k, v)| - a << "#{k}=#{v}" - end.join(',') - - stream.bgapi "originate {#{opts}}#{dial_command.to} &park()" - - dial_command.response = Ref.new uri: id - end - - def outbound? - direction == :outbound - end - - def inbound? - direction == :inbound - end - - def answered? - @answered - end - - def execute_command(command) - 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 - application 'respond', '180 Ringing' - command.response = true - when Command::Answer - if answered? - command.response = true - else - command_id = Punchblock.new_uuid - register_tmp_handler :es, :event_name => 'CHANNEL_ANSWER', [:[], :scope_variable_punchblock_command_id] => command_id do - command.response = true - end - application 'answer', "%[punchblock_command_id=#{command_id}]" - end - when Command::Hangup - hangup - command.response = true - when Command::Join - @pending_joins[command.call_uri] = command - uuid_foo :bridge, command.call_uri - when Command::Unjoin - @pending_unjoins[command.call_uri] = command - uuid_foo :transfer, '-both park inline' - when Command::Reject - hangup REJECT_TO_HANGUP_REASON[command.reason] - command.response = true - when Punchblock::Component::Output - media_renderer = command.renderer || :freeswitch - case media_renderer.to_s - when 'freeswitch', 'native' - execute_component Component::Output, command - when 'flite' - execute_component Component::FliteOutput, command - else - execute_component Component::TTSOutput, command - end - when Punchblock::Component::Input - execute_component Component::Input, 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 - end - - def hangup(reason = 'MANAGER_REQUEST') - sendmsg :call_command => 'hangup', :hangup_cause => reason - end - - def logger_id - "#{self.class}: #{id}" - end - - def actor_died(actor, reason) - return unless reason - pb_logger.error "A linked actor (#{actor.inspect}) died due to #{reason.inspect}" - if id = @components.key(actor) - @components.delete id - complete_event = Punchblock::Event::Complete.new :component_id => id, source_uri: id, :reason => Punchblock::Event::Complete::Error.new - send_pb_event complete_event - end - end - - private - - def send_end_event(reason) - send_pb_event Event::End.new(:reason => reason) - translator.deregister_call id - terminate - end - - def execute_component(type, command, *execute_args) - type.new_link(command, current_actor).tap do |component| - register_component component - component.execute(*execute_args) - 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 => es_env[:variable_sip_to_uri], - :from => "#{es_env[:variable_effective_caller_id_name]} <#{es_env[:variable_sip_from_uri]}>", - :headers => headers - end - - def headers - es_env.to_a.inject({}) do |accumulator, element| - accumulator['X-' + element[0].to_s] = element[1] || '' - accumulator - end - end - end - end - end -end diff --git a/lib/punchblock/translator/freeswitch/component.rb b/lib/punchblock/translator/freeswitch/component.rb deleted file mode 100644 index b7cdc5fd..00000000 --- a/lib/punchblock/translator/freeswitch/component.rb +++ /dev/null @@ -1,93 +0,0 @@ -# encoding: utf-8 - -module Punchblock - module Translator - class Freeswitch - module Component - extend ActiveSupport::Autoload - - autoload :AbstractOutput - autoload :FliteOutput - autoload :Input - autoload :Output - autoload :Record - autoload :TTSOutput - - class Component - include Celluloid - include DeadActorSafety - include HasGuardedHandlers - - extend ActorHasGuardedHandlers - execute_guarded_handlers_on_receiver - - attr_reader :id, :call, :call_id - - def initialize(component_node, call = nil) - @component_node, @call = component_node, call - @call_id = safe_from_dead_actors { call.id } if call - @id = Punchblock.new_uuid - @complete = false - setup - end - - def setup - end - - def execute_command(command) - command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command for component #{id}", call_id, id - end - - def handle_es_event(event) - trigger_handler :es, event - end - - def send_complete_event(reason, recording = nil) - return if @complete - @complete = true - event = Punchblock::Event::Complete.new reason: reason, recording: recording - send_event event - terminate - end - - def send_event(event) - event.component_id = id - event.target_call_id = call_id - event.source_uri = id - safe_from_dead_actors { translator.handle_pb_event event } - end - - def logger_id - "#{self.class}: #{call_id ? "Call ID: #{call_id}, Component ID: #{id}" : id}" - end - - def call_ended - send_complete_event Punchblock::Event::Complete::Hangup.new - end - - def application(appname, options = nil) - call.application appname, "%[punchblock_component_id=#{id}]#{options}" - end - - private - - def translator - call.translator - end - - def set_node_response(value) - @component_node.response = value - end - - def send_ref - set_node_response Ref.new uri: id - end - - def with_error(name, text) - set_node_response ProtocolError.new.setup(name, text) - end - end - end - end - end -end diff --git a/lib/punchblock/translator/freeswitch/component/abstract_output.rb b/lib/punchblock/translator/freeswitch/component/abstract_output.rb deleted file mode 100644 index 8e140b09..00000000 --- a/lib/punchblock/translator/freeswitch/component/abstract_output.rb +++ /dev/null @@ -1,63 +0,0 @@ -# encoding: utf-8 - -module Punchblock - module Translator - class Freeswitch - module Component - class AbstractOutput < Component - UnrenderableDocError = Class.new OptionError - - def execute - validate - send_ref - do_output - rescue UnrenderableDocError => e - with_error 'unrenderable document error', e.message - rescue OptionError => e - with_error 'option error', e.message - end - - def execute_command(command) - case command - when Punchblock::Component::Stop - command.response = true - application 'break' - send_complete_event Punchblock::Event::Complete::Stop.new - else - super - end - end - - private - - def do_output - raise 'Not Implemented' - end - - def validate - raise OptionError, 'An SSML document is required.' unless @component_node.render_documents.first.value - - [:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt| - raise OptionError, "A #{opt} value is unsupported." if @component_node.send opt - end - - case @component_node.interrupt_on - when :voice, :dtmf, :any - raise OptionError, "An interrupt-on value of #{@component_node.interrupt_on} is unsupported." - end - end - - def concatenated_render_doc - @component_node.render_documents.inject RubySpeech::SSML.draw do |doc, argument| - doc + argument.value - end - end - - def finish_reason - Punchblock::Component::Output::Complete::Finish.new - end - end - end - end - end -end diff --git a/lib/punchblock/translator/freeswitch/component/flite_output.rb b/lib/punchblock/translator/freeswitch/component/flite_output.rb deleted file mode 100644 index 1842a475..00000000 --- a/lib/punchblock/translator/freeswitch/component/flite_output.rb +++ /dev/null @@ -1,21 +0,0 @@ -# encoding: utf-8 - -module Punchblock - module Translator - class Freeswitch - module Component - class FliteOutput < TTSOutput - private - - def renderer - :flite - end - - def document - concatenated_render_doc.inner_text.to_s - end - end - end - end - end -end diff --git a/lib/punchblock/translator/freeswitch/component/input.rb b/lib/punchblock/translator/freeswitch/component/input.rb deleted file mode 100644 index 723fb752..00000000 --- a/lib/punchblock/translator/freeswitch/component/input.rb +++ /dev/null @@ -1,35 +0,0 @@ -# encoding: utf-8 - -module Punchblock - module Translator - class Freeswitch - module Component - class Input < Component - - include InputComponent - - def execute - super - @dtmf_handler_id = register_dtmf_event_handler - end - - private - - def register_dtmf_event_handler - component = current_actor - call.register_handler :es, :event_name => 'DTMF' do |event| - safe_from_dead_actors do - component.process_dtmf event[:dtmf_digit] - end - end - end - - def unregister_dtmf_event_handler - call.unregister_handler :es, @dtmf_handler_id if instance_variable_defined?(:@dtmf_handler_id) - rescue Celluloid::DeadActorError - end - end - end - end - end -end diff --git a/lib/punchblock/translator/freeswitch/component/output.rb b/lib/punchblock/translator/freeswitch/component/output.rb deleted file mode 100644 index a822805d..00000000 --- a/lib/punchblock/translator/freeswitch/component/output.rb +++ /dev/null @@ -1,56 +0,0 @@ -# encoding: utf-8 - -module Punchblock - module Translator - class Freeswitch - module Component - class Output < AbstractOutput - private - - def validate - super - filenames - end - - def do_output - playback "file_string://#{filenames.join('!')}" - end - - def filenames - @filenames ||= @component_node.render_documents.map do |doc| - doc.value.children.map do |node| - case node - when RubySpeech::SSML::Audio - node.src - when String - raise if node.include?(' ') - node - else - raise - end - end - end.compact.flatten - rescue - raise UnrenderableDocError, 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.' - end - - def playback(path) - register_handler :es, :event_name => 'CHANNEL_EXECUTE_COMPLETE' do |event| - send_complete_event complete_reason_for_event(event) - end - application 'playback', path - end - - def complete_reason_for_event(event) - case event[:application_response] - when 'FILE PLAYED' - finish_reason - else - Punchblock::Event::Complete::Error.new(:details => "Engine error: #{event[:application_response]}") - end - end - end - end - end - end -end diff --git a/lib/punchblock/translator/freeswitch/component/record.rb b/lib/punchblock/translator/freeswitch/component/record.rb deleted file mode 100644 index 60dd84cc..00000000 --- a/lib/punchblock/translator/freeswitch/component/record.rb +++ /dev/null @@ -1,93 +0,0 @@ -# encoding: utf-8 - -module Punchblock - module Translator - class Freeswitch - module Component - class Record < Component - RECORDING_BASE_PATH = '/var/punchblock/record' - - def setup - @complete_reason = nil - end - - def execute - max_duration = @component_node.max_duration || -1 - initial_timeout = @component_node.initial_timeout || -1 - final_timeout = @component_node.final_timeout || -1 - - raise OptionError, 'A start-beep value of true is unsupported.' if @component_node.start_beep - raise OptionError, 'A start-paused value of true is unsupported.' if @component_node.start_paused - raise OptionError, 'A max-duration value that is negative (and not -1) is invalid.' unless max_duration >= -1 - - @format = @component_node.format || 'wav' - - component = current_actor - call.register_handler :es, :event_name => 'RECORD_STOP', [:[], :record_file_path] => filename do |event| - component.finished - end - - record_args = ['start', filename] - record_args << max_duration/1000 unless max_duration == -1 - - direction = case @component_node.direction - when :send then :RECORD_WRITE_ONLY - when :recv then :RECORD_READ_ONLY - else :RECORD_STEREO - end - setvar direction, true - - setvar :RECORD_INITIAL_TIMEOUT_MS, initial_timeout > -1 ? initial_timeout : 0 - setvar :RECORD_FINAL_TIMEOUT_MS, final_timeout > -1 ? final_timeout : 0 - - call.uuid_foo :record, record_args.join(' ') - send_ref - rescue OptionError => e - with_error 'option error', e.message - end - - def execute_command(command) - case command - when Punchblock::Component::Stop - call.uuid_foo :record, "stop #{filename}" - @complete_reason = stop_reason - command.response = true - else - super - end - end - - def finished - send_complete_event(@complete_reason || max_duration_reason) - end - - private - - def setvar(key, value) - call.uuid_foo :setvar, "#{key} #{value}" - end - - def filename - File.join RECORDING_BASE_PATH, [id, @format].join('.') - end - - def recording - Punchblock::Component::Record::Recording.new :uri => "file://#{filename}" - end - - def stop_reason - Punchblock::Event::Complete::Stop.new - end - - def max_duration_reason - Punchblock::Component::Record::Complete::MaxDuration.new - end - - def send_complete_event(reason) - super reason, recording - end - end - end - end - end -end diff --git a/lib/punchblock/translator/freeswitch/component/tts_output.rb b/lib/punchblock/translator/freeswitch/component/tts_output.rb deleted file mode 100644 index 4f618e20..00000000 --- a/lib/punchblock/translator/freeswitch/component/tts_output.rb +++ /dev/null @@ -1,29 +0,0 @@ -# encoding: utf-8 - -module Punchblock - module Translator - class Freeswitch - module Component - class TTSOutput < AbstractOutput - private - - def do_output - register_handler :es, :event_name => 'CHANNEL_EXECUTE_COMPLETE' do |event| - send_complete_event finish_reason - end - voice = @component_node.voice || :kal - application :speak, [renderer, voice, document].join('|') - end - - def renderer - @component_node.renderer || :flite - end - - def document - concatenated_render_doc.to_s - end - end - end - end - end -end diff --git a/spec/punchblock/connection/freeswitch_spec.rb b/spec/punchblock/connection/freeswitch_spec.rb deleted file mode 100644 index 89e33060..00000000 --- a/spec/punchblock/connection/freeswitch_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -module Punchblock - module Connection - describe Freeswitch do - let :options do - { - :host => '127.0.0.1', - :port => 8021, - :password => 'test' - } - end - - let(:mock_event_handler) { double('Event Handler').as_null_object } - - let(:connection) { described_class.new options } - - let(:mock_stream) { double 'RubyFS::Stream' } - - subject { connection } - - before do - subject.event_handler = mock_event_handler - end - - it 'should set the connection on the translator' do - expect(subject.translator.connection).to be subject - end - - describe '#run' do - it 'starts a RubyFS stream' do - # subject.should_receive(:new_fs_stream).once.with('127.0.0.1', 8021, 'test').and_return mock_stream - expect(subject.stream).to receive(:run).once - expect { subject.run }.to raise_error(DisconnectedError) - end - end - - describe '#stop' do - it 'stops the RubyFS::Stream' do - expect(subject.stream).to receive(:shutdown).once - subject.stop - end - - it 'shuts down the translator' do - expect(subject.translator).to receive(:terminate).once - subject.stop - end - end - - it 'sends events from RubyFS to the translator' do - event = double 'RubyFS::Event' - expect(subject.translator.async).to receive(:handle_es_event).once.with event - expect(subject.translator.async).to receive(:handle_es_event).once.with RubyFS::Stream::Disconnected.new - subject.stream.fire_event event - end - - describe '#write' do - it 'sends a command to the translator' do - command = double 'Command' - options = {:foo => :bar} - expect(subject.translator.async).to receive(:execute_command).once.with command, options - subject.write command, options - end - end - - describe 'when a rayo event is received from the translator' do - it 'should call the event handler with the event' do - offer = Event::Offer.new - offer.target_call_id = '9f00061' - - expect(mock_event_handler).to receive(:call).once.with offer - subject.handle_event offer - end - end - end - end -end diff --git a/spec/punchblock/translator/freeswitch/call_spec.rb b/spec/punchblock/translator/freeswitch/call_spec.rb deleted file mode 100644 index 73129d52..00000000 --- a/spec/punchblock/translator/freeswitch/call_spec.rb +++ /dev/null @@ -1,1019 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -module Punchblock - module Translator - class Freeswitch - describe Call do - let(:id) { Punchblock.new_uuid } - let(:stream) { double('RubyFS::Stream').as_null_object } - let(:translator) { Freeswitch.new double('Connection::Freeswitch').as_null_object } - let(:es_env) do - { - :variable_direction => "inbound", - :variable_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f", - :variable_session_id => "1", - :variable_sip_local_network_addr => "109.148.160.137", - :variable_sip_network_ip => "192.168.1.74", - :variable_sip_network_port => "59253", - :variable_sip_received_ip => "192.168.1.74", - :variable_sip_received_port => "59253", - :variable_sip_via_protocol => "udp", - :variable_sip_authorized => "true", - :variable_sip_number_alias => "1000", - :variable_sip_auth_username => "1000", - :variable_sip_auth_realm => "127.0.0.1", - :variable_number_alias => "1000", - :variable_user_name => "1000", - :variable_domain_name => "127.0.0.1", - :variable_record_stereo => "true", - :variable_default_gateway => "example.com", - :variable_default_areacode => "918", - :variable_transfer_fallback_extension => "operator", - :variable_toll_allow => "domestic,international,local", - :variable_accountcode => "1000", - :variable_user_context => "default", - :variable_effective_caller_id_name => "Extension 1000", - :variable_effective_caller_id_number => "1000", - :variable_outbound_caller_id_name => "FreeSWITCH", - :variable_outbound_caller_id_number => "0000000000", - :variable_callgroup => "techsupport", - :variable_sip_from_user => "1000", - :variable_sip_from_uri => "1000@127.0.0.1", - :variable_sip_from_host => "127.0.0.1", - :variable_sip_from_user_stripped => "1000", - :variable_sip_from_tag => "1248111553", - :variable_sofia_profile_name => "internal", - :variable_sip_full_via => "SIP/2.0/UDP 192.168.1.74:59253;rport=59253;branch=z9hG4bK2021947958", - :variable_sip_full_from => ";tag=1248111553", - :variable_sip_full_to => "", - :variable_sip_req_user => "10", - :variable_sip_req_uri => "10@127.0.0.1", - :variable_sip_req_host => "127.0.0.1", - :variable_sip_to_user => "10", - :variable_sip_to_uri => "10@127.0.0.1", - :variable_sip_to_host => "127.0.0.1", - :variable_sip_contact_user => "1000", - :variable_sip_contact_port => "59253", - :variable_sip_contact_uri => "1000@192.168.1.74:59253", - :variable_sip_contact_host => "192.168.1.74", - :variable_channel_name => "sofia/internal/1000@127.0.0.1", - :variable_sip_call_id => "1251435211@127.0.0.1", - :variable_sip_user_agent => "YATE/4.1.0", - :variable_sip_via_host => "192.168.1.74", - :variable_sip_via_port => "59253", - :variable_sip_via_rport => "59253", - :variable_max_forwards => "20", - :variable_presence_id => "1000@127.0.0.1", - :variable_switch_r_sdp => "v=0\r\no=yate 1340801245 1340801245 IN IP4 172.20.10.3\r\ns=SIP Call\r\nc=IN IP4 172.20.10.3\r\nt=0 0\r\nm=audio 25048 RTP/AVP 0 8 11 98 97 102 103 104 105 106 101\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:11 L16/8000\r\na=rtpmap:98 iLBC/8000\r\na=fmtp:98 mode=20\r\na=rtpmap:97 iLBC/8000\r\na=fmtp:97 mode=30\r\na=rtpmap:102 SPEEX/8000\r\na=rtpmap:103 SPEEX/16000\r\na=rtpmap:104 SPEEX/32000\r\na=rtpmap:105 iSAC/16000\r\na=rtpmap:106 iSAC/32000\r\na=rtpmap:101 telephone-event/8000\r\na=ptime:30\r\n", - :variable_remote_media_ip => "172.20.10.3", - :variable_remote_media_port => "25048", - :variable_sip_audio_recv_pt => "0", - :variable_sip_use_codec_name => "PCMU", - :variable_sip_use_codec_rate => "8000", - :variable_sip_use_codec_ptime => "30", - :variable_read_codec => "PCMU", - :variable_read_rate => "8000", - :variable_write_codec => "PCMU", - :variable_write_rate => "8000", - :variable_endpoint_disposition => "RECEIVED", - :variable_call_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f", - :variable_open => "true", - :variable_rfc2822_date => "Wed, 27 Jun 2012 13:47:25 +0100", - :variable_export_vars => "RFC2822_DATE", - :variable_current_application => "park" - } - end - - let :headers do - { - 'X-variable_direction' => "inbound", - 'X-variable_uuid' => "3f0e1e18-c056-11e1-b099-fffeda3ce54f", - 'X-variable_session_id' => "1", - 'X-variable_sip_local_network_addr' => "109.148.160.137", - 'X-variable_sip_network_ip' => "192.168.1.74", - 'X-variable_sip_network_port' => "59253", - 'X-variable_sip_received_ip' => "192.168.1.74", - 'X-variable_sip_received_port' => "59253", - 'X-variable_sip_via_protocol' => "udp", - 'X-variable_sip_authorized' => "true", - 'X-variable_sip_number_alias' => "1000", - 'X-variable_sip_auth_username' => "1000", - 'X-variable_sip_auth_realm' => "127.0.0.1", - 'X-variable_number_alias' => "1000", - 'X-variable_user_name' => "1000", - 'X-variable_domain_name' => "127.0.0.1", - 'X-variable_record_stereo' => "true", - 'X-variable_default_gateway' => "example.com", - 'X-variable_default_areacode' => "918", - 'X-variable_transfer_fallback_extension' => "operator", - 'X-variable_toll_allow' => "domestic,international,local", - 'X-variable_accountcode' => "1000", - 'X-variable_user_context' => "default", - 'X-variable_effective_caller_id_name' => "Extension 1000", - 'X-variable_effective_caller_id_number' => "1000", - 'X-variable_outbound_caller_id_name' => "FreeSWITCH", - 'X-variable_outbound_caller_id_number' => "0000000000", - 'X-variable_callgroup' => "techsupport", - 'X-variable_sip_from_user' => "1000", - 'X-variable_sip_from_uri' => "1000@127.0.0.1", - 'X-variable_sip_from_host' => "127.0.0.1", - 'X-variable_sip_from_user_stripped' => "1000", - 'X-variable_sip_from_tag' => "1248111553", - 'X-variable_sofia_profile_name' => "internal", - 'X-variable_sip_full_via' => "SIP/2.0/UDP 192.168.1.74:59253;rport=59253;branch=z9hG4bK2021947958", - 'X-variable_sip_full_from' => ";tag=1248111553", - 'X-variable_sip_full_to' => "", - 'X-variable_sip_req_user' => "10", - 'X-variable_sip_req_uri' => "10@127.0.0.1", - 'X-variable_sip_req_host' => "127.0.0.1", - 'X-variable_sip_to_user' => "10", - 'X-variable_sip_to_uri' => "10@127.0.0.1", - 'X-variable_sip_to_host' => "127.0.0.1", - 'X-variable_sip_contact_user' => "1000", - 'X-variable_sip_contact_port' => "59253", - 'X-variable_sip_contact_uri' => "1000@192.168.1.74:59253", - 'X-variable_sip_contact_host' => "192.168.1.74", - 'X-variable_channel_name' => "sofia/internal/1000@127.0.0.1", - 'X-variable_sip_call_id' => "1251435211@127.0.0.1", - 'X-variable_sip_user_agent' => "YATE/4.1.0", - 'X-variable_sip_via_host' => "192.168.1.74", - 'X-variable_sip_via_port' => "59253", - 'X-variable_sip_via_rport' => "59253", - 'X-variable_max_forwards' => "20", - 'X-variable_presence_id' => "1000@127.0.0.1", - 'X-variable_switch_r_sdp' => "v=0\r\no=yate 1340801245 1340801245 IN IP4 172.20.10.3\r\ns=SIP Call\r\nc=IN IP4 172.20.10.3\r\nt=0 0\r\nm=audio 25048 RTP/AVP 0 8 11 98 97 102 103 104 105 106 101\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:11 L16/8000\r\na=rtpmap:98 iLBC/8000\r\na=fmtp:98 mode=20\r\na=rtpmap:97 iLBC/8000\r\na=fmtp:97 mode=30\r\na=rtpmap:102 SPEEX/8000\r\na=rtpmap:103 SPEEX/16000\r\na=rtpmap:104 SPEEX/32000\r\na=rtpmap:105 iSAC/16000\r\na=rtpmap:106 iSAC/32000\r\na=rtpmap:101 telephone-event/8000\r\na=ptime:30\r\n", - 'X-variable_remote_media_ip' => "172.20.10.3", - 'X-variable_remote_media_port' => "25048", - 'X-variable_sip_audio_recv_pt' => "0", - 'X-variable_sip_use_codec_name' => "PCMU", - 'X-variable_sip_use_codec_rate' => "8000", - 'X-variable_sip_use_codec_ptime' => "30", - 'X-variable_read_codec' => "PCMU", - 'X-variable_read_rate' => "8000", - 'X-variable_write_codec' => "PCMU", - 'X-variable_write_rate' => "8000", - 'X-variable_endpoint_disposition' => "RECEIVED", - 'X-variable_call_uuid' => "3f0e1e18-c056-11e1-b099-fffeda3ce54f", - 'X-variable_open' => "true", - 'X-variable_rfc2822_date' => "Wed, 27 Jun 2012 13:47:25 +0100", - 'X-variable_export_vars' => "RFC2822_DATE", - 'X-variable_current_application' => "park" - } - end - - subject { Call.new id, translator, es_env, stream } - - describe '#id' do - subject { super().id } - it { should be == id } - end - - describe '#translator' do - subject { super().translator } - it { should be translator } - end - - describe '#es_env' do - subject { super().es_env } - it { should be == es_env } - end - - describe '#stream' do - subject { super().stream } - it { should be stream } - end - - describe '#register_component' do - it 'should make the component accessible by ID' do - component_id = 'abc123' - component = double 'Translator::Freeswitch::Component', :id => component_id - subject.register_component component - expect(subject.component_with_id(component_id)).to be component - end - end - - describe '#send_offer' do - it 'sends an offer to the translator' do - expected_offer = Punchblock::Event::Offer.new :target_call_id => subject.id, - :to => "10@127.0.0.1", - :from => "Extension 1000 <1000@127.0.0.1>", - :headers => headers - expect(translator).to receive(:handle_pb_event).with expected_offer - subject.send_offer - end - - it 'should make the call identify as inbound' do - subject.send_offer - expect(subject.direction).to eq(:inbound) - expect(subject.inbound?).to be true - expect(subject.outbound?).to be false - end - end - - describe "#application" do - it "should execute a FS application on the current call" do - expect(stream).to receive(:application).once.with(id, 'appname', 'options') - subject.application 'appname', 'options' - end - end - - describe "#sendmsg" do - it "should execute a FS sendmsg on the current call" do - expect(stream).to receive(:sendmsg).once.with(id, 'msg', :foo => 'bar') - subject.sendmsg 'msg', :foo => 'bar' - end - end - - describe "#uuid_foo" do - it "should execute a FS uuid_* on the current call using bgapi" do - expect(stream).to receive(:bgapi).once.with("uuid_record #{id} blah.mp3") - subject.uuid_foo 'record', 'blah.mp3' - end - end - - describe '#dial' do - let(:dial_command_options) { {} } - - let(:to) { 'sofia/internal/1000' } - let(:from) { '1001' } - - let :dial_command do - Punchblock::Command::Dial.new({:to => to, :from => from}.merge(dial_command_options)) - end - - before { dial_command.request! } - - it 'sends an originate bgapi command' do - expect(stream).to receive(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id},origination_caller_id_number='#{from}'}#{to} &park()" - subject.dial dial_command - end - - context 'with a name and channel in the from field' do - let(:from_name) { 'Jane Smith' } - let(:from_number) { '1001' } - let(:from) { "#{from_name} <#{from_number}>" } - - it 'sends an originate bgapi command with the cid fields set correctly' do - expect(stream).to receive(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id},origination_caller_id_number='#{from_number}',origination_caller_id_name='#{from_name}'}#{to} &park()" - subject.dial dial_command - end - end - - context 'with a name and empty channel in the from field' do - let(:from_name) { 'Jane Smith' } - let(:from_number) { '' } - let(:from) { "#{from_name} <#{from_number}>" } - - it 'sends an originate bgapi command with the cid fields set correctly' do - expect(stream).to receive(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id},origination_caller_id_name='#{from_name}'}#{to} &park()" - subject.dial dial_command - end - end - - context 'with a number in the from field with angled brackets' do - let(:from_number) { '1001' } - let(:from) { "<#{from_number}>" } - - it 'sends an originate bgapi command with the cid fields set correctly' do - expect(stream).to receive(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id},origination_caller_id_number='#{from_number}'}#{to} &park()" - subject.dial dial_command - end - end - - context 'with an empty from attribute' do - let(:from) { '' } - - it 'sends an originate bgapi command with the cid fields set correctly' do - expect(stream).to receive(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id}}#{to} &park()" - subject.dial dial_command - end - end - - context 'with no from attribute' do - let(:from) { nil } - - it 'sends an originate bgapi command with the cid fields set correctly' do - expect(stream).to receive(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id}}#{to} &park()" - subject.dial dial_command - end - end - - context 'with a timeout specified' do - let :dial_command_options do - { :timeout => 10000 } - end - - it 'includes the timeout in the originate command' do - expect(stream).to receive(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id},origination_caller_id_number='#{from}',originate_timeout=10}#{to} &park()" - subject.dial dial_command - end - end - - context 'with headers specified' do - let :dial_command_options do - { :headers => {'X-foo' => 'bar', 'X-doo' => 'dah'} } - end - - it 'includes the headers in the originate command' do - expect(stream).to receive(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id},origination_caller_id_number='#{from}',sip_h_X-foo='bar',sip_h_X-doo='dah'}#{to} &park()" - subject.dial dial_command - end - end - - it 'sends the call ID as a response to the Dial' do - subject.dial dial_command - dial_command.response - expect(dial_command.target_call_id).to eq(subject.id) - end - - it 'should make the call identify as outbound' do - subject.dial dial_command - expect(subject.direction).to eq(:outbound) - expect(subject.outbound?).to be true - expect(subject.inbound?).to be false - end - end - - describe '#handle_es_event' do - context 'with a CHANNEL_HANGUP event' do - let :es_event do - RubyFS::Event.new nil, :event_name => "CHANNEL_HANGUP", - :hangup_cause => cause, - :channel_state => "CS_HANGUP", - :channel_call_state => "HANGUP", - :channel_state_number => "10", - :unique_id => "756bdd8e-c064-11e1-b0ac-fffeda3ce54f", - :answer_state => "hangup", - :variable_sip_term_status => "487", - :variable_proto_specific_hangup_cause => "sip%3A487", - :variable_sip_term_cause => "487" - end - - let(:cause) { 'ORIGINATOR_CANCEL' } - - it "should cause the actor to be terminated" do - expect(translator).to receive(:handle_pb_event).once - subject.handle_es_event es_event - sleep 0.25 - expect(subject.alive?).to be false - end - - it "de-registers the call from the translator" do - allow(translator).to receive :handle_pb_event - expect(translator).to receive(:deregister_call).once.with(id) - subject.handle_es_event es_event - end - - it "should cause all components to send complete events before sending end event" do - ssml_doc = RubySpeech::SSML.draw { audio { 'foo.wav' } } - comp_command = Punchblock::Component::Output.new :render_document => {:value => ssml_doc} - comp_command.request! - component = subject.execute_command comp_command - expect(comp_command.response(0.1)).to be_a Ref - - expected_complete_event = Punchblock::Event::Complete.new :target_call_id => subject.id, :component_id => component.id, source_uri: component.id - expected_complete_event.reason = Punchblock::Event::Complete::Hangup.new - expected_end_event = Punchblock::Event::End.new :reason => :hangup, :target_call_id => subject.id - - expect(translator).to receive(:handle_pb_event).with(expected_complete_event).once.ordered - expect(translator).to receive(:handle_pb_event).with(expected_end_event).once.ordered - subject.handle_es_event es_event - end - - [ - 'NORMAL_CLEARING', - 'ORIGINATOR_CANCEL', - 'SYSTEM_SHUTDOWN', - 'BLIND_TRANSFER', - 'ATTENDED_TRANSFER', - 'PICKED_OFF', - 'NORMAL_UNSPECIFIED' - ].each do |cause| - context "with a #{cause} cause" do - let(:cause) { cause } - - it 'should send an end (hangup) event to the translator' do - expected_end_event = Punchblock::Event::End.new :reason => :hangup, - :target_call_id => subject.id - expect(translator).to receive(:handle_pb_event).with expected_end_event - subject.handle_es_event es_event - end - end - end - - context "with a MANAGER_REQUEST cause" do - let(:cause) { 'MANAGER_REQUEST' } - - it 'should send an end (hangup-command) event to the translator' do - expected_end_event = Punchblock::Event::End.new :reason => :hangup_command, - :target_call_id => subject.id - expect(translator).to receive(:handle_pb_event).with expected_end_event - subject.handle_es_event es_event - end - end - - context "with a user busy cause" do - let(:cause) { 'USER_BUSY' } - - it 'should send an end (busy) event to the translator' do - expected_end_event = Punchblock::Event::End.new :reason => :busy, - :target_call_id => subject.id - expect(translator).to receive(:handle_pb_event).with expected_end_event - subject.handle_es_event es_event - end - end - - [ - 'NO_USER_RESPONSE', - 'NO_ANSWER', - 'SUBSCRIBER_ABSENT', - 'ALLOTTED_TIMEOUT', - 'MEDIA_TIMEOUT', - 'PROGRESS_TIMEOUT' - ].each do |cause| - context "with a #{cause} cause" do - let(:cause) { cause } - - it 'should send an end (timeout) event to the translator' do - expected_end_event = Punchblock::Event::End.new :reason => :timeout, - :target_call_id => subject.id - expect(translator).to receive(:handle_pb_event).with expected_end_event - subject.handle_es_event es_event - end - end - end - - [ - 'CALL_REJECTED', - 'NUMBER_CHANGED', - 'REDIRECTION_TO_NEW_DESTINATION', - 'FACILITY_REJECTED', - 'NORMAL_CIRCUIT_CONGESTION', - 'SWITCH_CONGESTION', - 'USER_NOT_REGISTERED', - 'FACILITY_NOT_SUBSCRIBED', - 'OUTGOING_CALL_BARRED', - 'INCOMING_CALL_BARRED', - 'BEARERCAPABILITY_NOTAUTH', - 'BEARERCAPABILITY_NOTAVAIL', - 'SERVICE_UNAVAILABLE', - 'BEARERCAPABILITY_NOTIMPL', - 'CHAN_NOT_IMPLEMENTED', - 'FACILITY_NOT_IMPLEMENTED', - 'SERVICE_NOT_IMPLEMENTED' - ].each do |cause| - context "with a #{cause} cause" do - let(:cause) { cause } - - it 'should send an end (reject) event to the translator' do - expected_end_event = Punchblock::Event::End.new :reason => :reject, - :target_call_id => subject.id - expect(translator).to receive(:handle_pb_event).with expected_end_event - subject.handle_es_event es_event - end - end - end - - [ - "UNSPECIFIED", - "UNALLOCATED_NUMBER", - "NO_ROUTE_TRANSIT_NET", - "NO_ROUTE_DESTINATION", - "CHANNEL_UNACCEPTABLE", - "CALL_AWARDED_DELIVERED", - "EXCHANGE_ROUTING_ERROR", - "DESTINATION_OUT_OF_ORDER", - "INVALID_NUMBER_FORMAT", - "RESPONSE_TO_STATUS_ENQUIRY", - "NETWORK_OUT_OF_ORDER", - "NORMAL_TEMPORARY_FAILURE", - "ACCESS_INFO_DISCARDED", - "REQUESTED_CHAN_UNAVAIL", - "PRE_EMPTED", - "INVALID_CALL_REFERENCE", - "INCOMPATIBLE_DESTINATION", - "INVALID_MSG_UNSPECIFIED", - "MESSAGE_TYPE_NONEXIST", - "WRONG_MESSAGE", - "IE_NONEXIST", - "INVALID_IE_CONTENTS", - "WRONG_CALL_STATE", - "RECOVERY_ON_TIMER_EXPIRE", - "MANDATORY_IE_LENGTH_ERROR", - "PROTOCOL_ERROR", - "INTERWORKING", - "CRASH", - "LOSE_RACE", - "USER_CHALLENGE" - ].each do |cause| - context "with a #{cause} cause" do - let(:cause) { cause } - - it 'should send an end (error) event to the translator' do - expected_end_event = Punchblock::Event::End.new :reason => :error, - :target_call_id => subject.id - expect(translator).to receive(:handle_pb_event).with expected_end_event - subject.handle_es_event es_event - end - end - end - end - - context 'with an event for a known component' do - let(:mock_component_node) { double 'Punchblock::Component::Output' } - let :component do - Component::Output.new mock_component_node, subject - end - - let(:es_event) do - RubyFS::Event.new nil, :scope_variable_punchblock_component_id => component.id - end - - before do - subject.register_component component - end - - it 'should send the event to the component' do - expect(component).to receive(:handle_es_event).once.with es_event - subject.handle_es_event es_event - end - end - - context 'with a CHANNEL_STATE event' do - let :es_event do - RubyFS::Event.new nil, { - :event_name => 'CHANNEL_STATE', - :channel_call_state => channel_call_state - } - end - - context 'ringing' do - let(:channel_call_state) { 'RINGING' } - - it 'should send a ringing event' do - expected_ringing = Punchblock::Event::Ringing.new - expected_ringing.target_call_id = subject.id - expect(translator).to receive(:handle_pb_event).with expected_ringing - subject.handle_es_event es_event - end - - it '#answered? should return false' do - subject.handle_es_event es_event - expect(subject).not_to be_answered - end - end - - context 'something else' do - let(:channel_call_state) { 'FOO' } - - it 'should not send a ringing event' do - expect(translator).to receive(:handle_pb_event).never - subject.handle_es_event es_event - end - - it '#answered? should return false' do - subject.handle_es_event es_event - expect(subject).not_to be_answered - end - end - end - - context 'with a CHANNEL_ANSWER event' do - let :es_event do - RubyFS::Event.new nil, :event_name => 'CHANNEL_ANSWER' - end - - it 'should send an answered event' do - expected_answered = Punchblock::Event::Answered.new - expected_answered.target_call_id = subject.id - expect(translator).to receive(:handle_pb_event).with expected_answered - subject.handle_es_event es_event - end - - it '#answered? should be true' do - subject.handle_es_event es_event - expect(subject).to be_answered - end - end - - context 'with a handler registered for a matching event' do - let :es_event do - RubyFS::Event.new nil, :event_name => 'DTMF' - end - - let(:response) { double 'Response' } - - it 'should execute the handler' do - expect(response).to receive(:call).once.with es_event - subject.register_handler :es, :event_name => 'DTMF' do |event| - response.call event - end - subject.handle_es_event es_event - end - end - - context 'with a CHANNEL_BRIDGE event' do - let(:other_call_id) { Punchblock.new_uuid } - - let :expected_joined do - Punchblock::Event::Joined.new target_call_id: subject.id, - call_uri: other_call_id - end - - context "where this is the joining call" do - let :bridge_event do - RubyFS::Event.new nil, { - :unique_id => id, - :event_name => 'CHANNEL_BRIDGE', - :other_leg_unique_id => other_call_id - } - end - - it "should send a joined event with the correct call ID" do - expect(translator).to receive(:handle_pb_event).with expected_joined - subject.handle_es_event bridge_event - end - end - - context "where this is the joined call" do - let :bridge_event do - RubyFS::Event.new nil, { - :unique_id => other_call_id, - :event_name => 'CHANNEL_BRIDGE', - :other_leg_unique_id => id - } - end - - it "should send a joined event with the correct call ID" do - expect(translator).to receive(:handle_pb_event).with expected_joined - subject.handle_es_event bridge_event - end - end - end - - context 'with a CHANNEL_UNBRIDGE event' do - let(:other_call_id) { Punchblock.new_uuid } - - let :expected_unjoined do - Punchblock::Event::Unjoined.new target_call_id: subject.id, - call_uri: other_call_id - end - - context "where this is the unjoining call" do - let :unbridge_event do - RubyFS::Event.new nil, { - :unique_id => id, - :event_name => 'CHANNEL_UNBRIDGE', - :other_leg_unique_id => other_call_id - } - end - - it "should send a unjoined event with the correct call ID" do - expect(translator).to receive(:handle_pb_event).with expected_unjoined - subject.handle_es_event unbridge_event - end - end - - context "where this is the joined call" do - let :unbridge_event do - RubyFS::Event.new nil, { - :unique_id => other_call_id, - :event_name => 'CHANNEL_UNBRIDGE', - :other_leg_unique_id => id - } - end - - it "should send a unjoined event with the correct call ID" do - expect(translator).to receive(:handle_pb_event).with expected_unjoined - subject.handle_es_event unbridge_event - end - end - end - end - - describe '#execute_command' do - before do - command.request! - end - - context 'with an accept command' do - let(:command) { Command::Accept.new } - - it "should send a respond 180 command and set the command's response" do - expect(subject.wrapped_object).to receive(:application).once.with('respond', '180 Ringing') - subject.execute_command command - expect(command.response(0.5)).to be true - end - end - - context 'with an answer command' do - let(:command) { Command::Answer.new } - - it "should execute the answer application and set the command's response" do - subject - expect(Punchblock).to receive(:new_uuid).once.and_return 'abc123' - expect(subject.wrapped_object).to receive(:application).once.with('answer', "%[punchblock_command_id=abc123]") - expect(subject).not_to be_answered - subject.execute_command command - subject.handle_es_event RubyFS::Event.new(nil, :event_name => 'CHANNEL_ANSWER', :scope_variable_punchblock_command_id => 'abc123') - expect(command.response(0.5)).to be true - expect(subject).to be_answered - end - - it "should not execute the answer application twice if already answered" do - subject - expect(Punchblock).to receive(:new_uuid).once.and_return 'abc123' - expect(subject.wrapped_object).to receive(:application).once.with('answer', "%[punchblock_command_id=abc123]") - expect(subject).not_to be_answered - subject.execute_command command - subject.handle_es_event RubyFS::Event.new(nil, :event_name => 'CHANNEL_ANSWER', :scope_variable_punchblock_command_id => 'abc123') - expect(command.response(0.5)).to be true - expect(subject).to be_answered - subject.execute_command command - end - - context "when a component has previously been executed" do - it "should set the answer command's response correctly" do - subject - expect(Punchblock).to receive(:new_uuid).once.and_return 'abc123' - expect(subject.wrapped_object).to receive(:application).once.with('answer', "%[punchblock_command_id=abc123]") - expect(subject).not_to be_answered - subject.execute_command command - subject.handle_es_event RubyFS::Event.new(nil, :event_name => 'CHANNEL_ANSWER', :scope_variable_punchblock_command_id => 'abc123', :scope_variable_punchblock_component_id => 'dj182989j') - expect(command.response(0.5)).to be true - expect(subject).to be_answered - end - end - end - - def expect_hangup_with_reason(reason) - expect(subject.wrapped_object).to receive(:sendmsg).once.with(:call_command => 'hangup', :hangup_cause => reason) - end - - context 'with a hangup command' do - let(:command) { Command::Hangup.new } - - it "should send a hangup message and set the command's response" do - expect_hangup_with_reason 'MANAGER_REQUEST' - subject.execute_command command - expect(command.response(0.5)).to be true - end - end - - context 'with a reject command' do - let(:command) { Command::Reject.new } - - it "with a :busy reason should send a USER_BUSY hangup command and set the command's response" do - command.reason = :busy - expect_hangup_with_reason 'USER_BUSY' - subject.execute_command command - expect(command.response(0.5)).to be true - end - - it "with a :decline reason should send a CALL_REJECTED hangup command and set the command's response" do - command.reason = :decline - expect_hangup_with_reason 'CALL_REJECTED' - subject.execute_command command - expect(command.response(0.5)).to be true - end - - it "with an :error reason should send a NORMAL_TEMPORARY_FAILURE hangup command and set the command's response" do - command.reason = :error - expect_hangup_with_reason 'NORMAL_TEMPORARY_FAILURE' - subject.execute_command command - expect(command.response(0.5)).to be true - end - end - - context 'with an Output component' do - let :command do - Punchblock::Component::Output.new renderer: renderer - end - - let(:mock_component) { Translator::Freeswitch::Component::Output.new(command, subject) } - - ['freeswitch', 'native', nil].each do |renderer| - let(:renderer) { renderer } - - context "with a renderer of #{renderer}" do - it 'should create an Output component and execute it asynchronously' do - expect(Component::Output).to receive(:new_link).once.with(command, subject).and_return mock_component - expect(mock_component).to receive(:execute).once - subject.execute_command command - expect(subject.component_with_id(mock_component.id)).to be mock_component - end - end - end - - context 'with the renderer of :flite' do - let(:renderer) { :flite } - - it 'should create a FliteOutput component and execute it asynchronously using flite and the calls default voice' do - expect(Component::FliteOutput).to receive(:new_link).once.with(command, subject).and_return mock_component - expect(mock_component).to receive(:execute).once - subject.execute_command command - expect(subject.component_with_id(mock_component.id)).to be mock_component - end - end - - context 'with the renderer of :cepstral' do - let(:renderer) { :cepstral } - - it 'should create a TTSOutput component and execute it asynchronously using cepstral and the calls default voice' do - expect(Component::TTSOutput).to receive(:new_link).once.with(command, subject).and_return mock_component - expect(mock_component).to receive(:execute).once - subject.execute_command command - expect(subject.component_with_id(mock_component.id)).to be mock_component - end - end - - context 'with the renderer of :unimrcp' do - let(:renderer) { :unimrcp } - - it 'should create a TTSOutput component and execute it asynchronously using unimrcp and the calls default voice' do - expect(Component::TTSOutput).to receive(:new_link).once.with(command, subject).and_return mock_component - expect(mock_component).to receive(:execute).once - subject.execute_command command - expect(subject.component_with_id(mock_component.id)).to be mock_component - end - end - end - - context 'with an Input component' do - let :command do - Punchblock::Component::Input.new - end - - let(:mock_component) { Translator::Freeswitch::Component::Input.new(command, subject) } - - it 'should create an Input component and execute it asynchronously' do - expect(Component::Input).to receive(:new_link).once.with(command, subject).and_return mock_component - expect(mock_component).to receive(:execute).once - subject.execute_command command - end - end - - context 'with a Record component' do - let :command do - Punchblock::Component::Record.new - end - - let(:mock_component) { Translator::Freeswitch::Component::Record.new(command, subject) } - - it 'should create a Record component and execute it asynchronously' do - expect(Component::Record).to receive(:new_link).once.with(command, subject).and_return mock_component - expect(mock_component).to receive(:execute).once - subject.execute_command command - end - end - - context 'with a component command' do - let(:component_id) { 'foobar' } - - let :command do - Punchblock::Component::Stop.new :component_id => component_id - end - - let :mock_component do - double 'Component', :id => component_id - end - - context "for a known component ID" do - before { subject.register_component mock_component } - - it 'should send the command to the component for execution' do - expect(mock_component).to receive(:execute_command).once - subject.execute_command command - end - end - - context "for a component which began executing but crashed" do - let :component_command do - Punchblock::Component::Output.new :render_document => {:value => RubySpeech::SSML.draw} - end - - let(:comp_id) { component_command.response.component_id } - - let(:subsequent_command) { Punchblock::Component::Stop.new :component_id => comp_id } - - let :expected_event do - Punchblock::Event::Complete.new target_call_id: subject.id, - component_id: comp_id, - source_uri: comp_id, - reason: Punchblock::Event::Complete::Error.new - end - - before do - component_command.request! - subject.execute_command component_command - end - - it 'sends an error in response to the command' do - component = subject.component_with_id comp_id - - component.wrapped_object.define_singleton_method(:oops) do - raise 'Woops, I died' - end - - expect(translator).to receive(:handle_pb_event).once.with expected_event - - expect { component.oops }.to raise_error(/Woops, I died/) - sleep 0.1 - expect(component.alive?).to be false - expect(subject.component_with_id(comp_id)).to be_nil - - subsequent_command.request! - subject.execute_command subsequent_command - expect(subsequent_command.response).to eq(ProtocolError.new.setup(:item_not_found, "Could not find a component with ID #{comp_id} for call #{subject.id}", subject.id, comp_id)) - end - end - - context "for an unknown component ID" do - it 'sends an error in response to the command' do - subject.execute_command command - expect(command.response).to eq(ProtocolError.new.setup(:item_not_found, "Could not find a component with ID #{component_id} for call #{subject.id}", subject.id, component_id)) - end - end - end - - context 'with a command we do not understand' do - let :command do - Punchblock::Command::Mute.new - end - - it 'sends an error in response to the command' do - subject.execute_command command - expect(command.response).to eq(ProtocolError.new.setup('command-not-acceptable', "Did not understand command for call #{subject.id}", subject.id)) - end - end - - context "with a join command" do - let(:other_call_id) { Punchblock.new_uuid } - - let :command do - Punchblock::Command::Join.new :call_uri => other_call_id - end - - it "executes the proper uuid_bridge command" do - expect(subject.wrapped_object).to receive(:uuid_foo).once.with :bridge, other_call_id - subject.execute_command command - expect { command.response 1 }.to raise_exception(Timeout::Error) - end - - context "subsequently receiving a CHANNEL_BRIDGE event" do - let :bridge_event do - RubyFS::Event.new nil, { - :event_name => 'CHANNEL_BRIDGE', - :other_leg_unique_id => other_call_id - } - end - - before do - subject.execute_command command - end - - it "should set the command response to true" do - subject.handle_es_event bridge_event - expect(command.response).to be_true - end - end - end - - context "with an unjoin command" do - let(:other_call_id) { Punchblock.new_uuid } - - let :command do - Punchblock::Command::Unjoin.new :call_uri => other_call_id - end - - it "executes the unjoin via transfer to park" do - expect(subject.wrapped_object).to receive(:uuid_foo).once.with :transfer, '-both park inline' - subject.execute_command command - expect { command.response 1 }.to raise_exception(Timeout::Error) - end - - context "subsequently receiving a CHANNEL_UNBRIDGE event" do - let :unbridge_event do - RubyFS::Event.new nil, { - :event_name => 'CHANNEL_UNBRIDGE', - :other_leg_unique_id => other_call_id - } - end - - before do - subject.execute_command command - end - - it "should set the command response to true" do - subject.handle_es_event unbridge_event - expect(command.response).to be_true - end - end - end - end - end - end - end -end diff --git a/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb b/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb deleted file mode 100644 index 5471a683..00000000 --- a/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +++ /dev/null @@ -1,283 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -module Punchblock - module Translator - class Freeswitch - module Component - describe FliteOutput do - include HasMockCallbackConnection - - let(:translator) { Punchblock::Translator::Freeswitch.new connection } - let(:mock_call) { Punchblock::Translator::Freeswitch::Call.new 'foo', translator } - - let :original_command do - Punchblock::Component::Output.new command_options - end - - let :ssml_doc do - RubySpeech::SSML.draw do - say_as(:interpret_as => :cardinal) { 'FOO' } - end - end - - let :command_options do - { :render_document => {:value => ssml_doc} } - end - - def execute - subject.execute - end - - subject { described_class.new original_command, mock_call } - - describe '#execute' do - before { original_command.request! } - def expect_playback(voice = :kal, doc = "FOO") - expect(subject.wrapped_object).to receive(:application).once.with :speak, "flite|#{voice}|#{doc}" - end - - let(:command_opts) { {} } - - let :command_options do - { :render_document => {:value => ssml_doc} }.merge(command_opts) - end - - let :original_command do - Punchblock::Component::Output.new command_options - end - - describe 'document' do - context 'unset' do - let(:ssml_doc) { nil } - it "should return an error and not execute any actions" do - execute - error = ProtocolError.new.setup 'option error', 'An SSML document is required.' - expect(original_command.response(0.1)).to eq(error) - end - end - - context 'with an SSML node' do - it 'should speak the document using the speak application' do - expect_playback - execute - end - - it 'should send a complete event when the speak finishes' do - expect_playback - execute - subject.handle_es_event RubyFS::Event.new(nil, :event_name => "CHANNEL_EXECUTE_COMPLETE") - expect(original_command.complete_event(0.1).reason).to be_a Punchblock::Component::Output::Complete::Finish - end - end - - context 'with multiple documents' do - let(:command_opts) { { :render_documents => [{:value => ssml_doc}, {:value => ssml_doc}] } } - - it "should render all audio files from all documents" do - expect_playback :kal, "FOOFOO" - subject.execute - end - end - end - - describe 'start-offset' do - context 'unset' do - let(:command_opts) { { :start_offset => nil } } - it 'should not pass any options to Playback' do - expect_playback - execute - end - end - - context 'set' do - let(:command_opts) { { :start_offset => 10 } } - it "should return an error and not execute any actions" do - execute - error = ProtocolError.new.setup 'option error', 'A start_offset value is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - - describe 'start-paused' do - context 'false' do - let(:command_opts) { { :start_paused => false } } - it 'should not pass any options to Playback' do - expect_playback - execute - end - end - - context 'true' do - let(:command_opts) { { :start_paused => true } } - it "should return an error and not execute any actions" do - execute - error = ProtocolError.new.setup 'option error', 'A start_paused value is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - - describe 'repeat-interval' do - context 'unset' do - let(:command_opts) { { :repeat_interval => nil } } - it 'should not pass any options to Playback' do - expect_playback - execute - end - end - - context 'set' do - let(:command_opts) { { :repeat_interval => 10 } } - it "should return an error and not execute any actions" do - execute - error = ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - - describe 'repeat-times' do - context 'unset' do - let(:command_opts) { { :repeat_times => nil } } - it 'should not pass any options to Playback' do - expect_playback - execute - end - end - - context 'set' do - let(:command_opts) { { :repeat_times => 2 } } - it "should return an error and not execute any actions" do - execute - error = ProtocolError.new.setup 'option error', 'A repeat_times value is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - - describe 'max-time' do - context 'unset' do - let(:command_opts) { { :max_time => nil } } - it 'should not pass any options to Playback' do - expect_playback - execute - end - end - - context 'set' do - let(:command_opts) { { :max_time => 30 } } - it "should return an error and not execute any actions" do - execute - error = ProtocolError.new.setup 'option error', 'A max_time value is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - - describe 'voice' do - context 'unset' do - let(:command_opts) { { :voice => nil } } - it 'should use the default voice' do - expect_playback - execute - end - end - - context 'set' do - let(:command_opts) { { :voice => 'alison' } } - it "should execute speak with the specified voice" do - expect_playback 'alison' - execute - end - end - end - - describe 'interrupt_on' do - context "set to nil" do - let(:command_opts) { { :interrupt_on => nil } } - it "should not pass any digits to Playback" do - expect_playback - execute - end - end - - context "set to :any" do - let(:command_opts) { { :interrupt_on => :any } } - it "should return an error and not execute any actions" do - execute - error = ProtocolError.new.setup 'option error', 'An interrupt-on value of any is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - - context "set to :dtmf" do - let(:command_opts) { { :interrupt_on => :dtmf } } - it "should return an error and not execute any actions" do - execute - error = ProtocolError.new.setup 'option error', 'An interrupt-on value of dtmf is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - - context "set to :voice" do - let(:command_opts) { { :interrupt_on => :voice } } - it "should return an error and not execute any actions" do - execute - error = ProtocolError.new.setup 'option error', 'An interrupt-on value of voice is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - end - - describe "#execute_command" do - context "with a command it does not understand" do - let(:command) { Punchblock::Component::Output::Pause.new } - - before { command.request! } - - it "returns a ProtocolError response" do - subject.execute_command command - expect(command.response(0.1)).to be_a ProtocolError - end - end - - context "with a Stop command" do - let(:command) { Punchblock::Component::Stop.new } - let(:reason) { original_command.complete_event(5).reason } - - before do - command.request! - original_command.request! - original_command.execute! - end - - it "sets the command response to true" do - expect(subject.wrapped_object).to receive(:application) - subject.execute_command command - expect(command.response(0.1)).to eq(true) - end - - it "sends the correct complete event" do - expect(subject.wrapped_object).to receive(:application) - expect(original_command).not_to be_complete - subject.execute_command command - expect(reason).to be_a Punchblock::Event::Complete::Stop - expect(original_command).to be_complete - end - - it "breaks the current dialplan application" do - expect(subject.wrapped_object).to receive(:application).once.with 'break' - subject.execute_command command - end - end - end - end - - end - end - end -end diff --git a/spec/punchblock/translator/freeswitch/component/input_spec.rb b/spec/punchblock/translator/freeswitch/component/input_spec.rb deleted file mode 100644 index b39fbc0d..00000000 --- a/spec/punchblock/translator/freeswitch/component/input_spec.rb +++ /dev/null @@ -1,386 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -module Punchblock - module Translator - class Freeswitch - module Component - describe Input do - include HasMockCallbackConnection - - let(:id) { Punchblock.new_uuid } - let(:translator) { Punchblock::Translator::Freeswitch.new connection } - let(:mock_stream) { double('RubyFS::Stream') } - let(:call) { Punchblock::Translator::Freeswitch::Call.new id, translator, nil, mock_stream } - - let(:original_command_options) { {} } - - let :original_command do - Punchblock::Component::Input.new original_command_options - end - - let :grammar do - RubySpeech::GRXML.draw :mode => 'dtmf', :root => 'pin' do - rule id: 'digit' do - one_of do - 0.upto(9) { |d| item { d.to_s } } - end - end - - rule id: 'pin', scope: 'public' do - item repeat: '2' do - ruleref uri: '#digit' - end - end - end - end - - subject { Input.new original_command, call } - - describe '#execute' do - before { original_command.request! } - - let(:original_command_opts) { {} } - - let :original_command_options do - { :mode => :dtmf, :grammar => { :value => grammar } }.merge(original_command_opts) - end - - def dtmf_event(digit) - RubyFS::Event.new nil, { - :event_name => 'DTMF', - :dtmf_digit => digit.to_s, - :dtmf_duration => "1600" - } - end - - def send_dtmf(digit) - call.handle_es_event dtmf_event(digit) - end - - let(:reason) { original_command.complete_event(5).reason } - - describe "receiving DTMF events" do - before do - subject.execute - expected_event - end - - context "when a match is found" do - before do - send_dtmf 1 - send_dtmf 2 - sleep 0.5 - end - - let :expected_nlsml do - RubySpeech::NLSML.draw do - interpretation confidence: 1 do - instance "dtmf-1 dtmf-2" - input "12", mode: :dtmf - end - end - end - - let :expected_event do - Punchblock::Component::Input::Complete::Match.new nlsml: expected_nlsml - end - - it "should send a success complete event with the relevant data" do - expect(reason).to eq(expected_event) - end - - it "should not process further dtmf events" do - expect(subject.async).to receive(:process_dtmf).never - send_dtmf 3 - end - end - - context "when the match is invalid" do - before do - send_dtmf 1 - send_dtmf '#' - end - - let :expected_event do - Punchblock::Component::Input::Complete::NoMatch.new - end - - it "should send a nomatch complete event" do - expect(reason).to eq(expected_event) - end - end - end - - describe 'grammar' do - context 'unset' do - let(:original_command_opts) { { :grammar => nil } } - it "should return an error and not execute any actions" do - subject.execute - error = ProtocolError.new.setup 'option error', 'A grammar document is required.' - expect(original_command.response(0.1)).to eq(error) - end - end - - context 'with a builtin grammar' do - let(:original_command_opts) { { grammar: { url: 'builtin:dtmf/boolean' } } } - - before do - subject.execute - expected_event - send_dtmf 1 - end - - let :expected_nlsml do - RubySpeech::NLSML.draw do - interpretation confidence: 1 do - instance "true" - input "1", mode: :dtmf - end - end - end - - let :expected_event do - Punchblock::Component::Input::Complete::Match.new nlsml: expected_nlsml - end - - it "should use RubySpeech builtin grammar" do - expect(reason).to eq(expected_event) - end - end - - context 'with a parameterized builtin grammar' do - let(:original_command_opts) { { grammar: { url: 'builtin:dtmf/boolean?n=3;y=4' } } } - - before do - subject.execute - expected_event - send_dtmf 4 - end - - let :expected_nlsml do - RubySpeech::NLSML.draw do - interpretation confidence: 1 do - instance "true" - input "4", mode: :dtmf - end - end - end - - let :expected_event do - Punchblock::Component::Input::Complete::Match.new nlsml: expected_nlsml - end - - it "should use RubySpeech builtin grammar" do - expect(reason).to eq(expected_event) - end - end - - context 'with a bad builtin grammar name' do - let(:original_command_opts) { { grammar: { url: 'builtin:dtmf/foobar' } } } - - it "should return an error and not execute any actions" do - subject.execute - error = ProtocolError.new.setup 'option error', 'foobar is an invalid builtin grammar' - expect(original_command.response(0.1)).to eq(error) - end - end - - context 'with multiple grammars' do - let(:original_command_opts) { { :grammars => [{:value => grammar}, {:value => grammar}] } } - it "should return an error and not execute any actions" do - subject.execute - error = ProtocolError.new.setup 'option error', 'Only a single grammar is supported.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - - describe 'mode' do - context 'unset' do - let(:original_command_opts) { { :mode => nil } } - it "should return an error and not execute any actions" do - subject.execute - error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - - context 'any' do - let(:original_command_opts) { { :mode => :any } } - it "should return an error and not execute any actions" do - subject.execute - error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - - context 'voice' do - let(:original_command_opts) { { :mode => :voice } } - it "should return an error and not execute any actions" do - subject.execute - error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - - describe 'terminator' do - pending - end - - describe 'recognizer' do - pending - end - - describe 'initial-timeout' do - context 'a positive number' do - let(:original_command_opts) { { :initial_timeout => 1000 } } - - it "should not cause a NoInput if first input is received in time" do - subject.execute - send_dtmf 1 - sleep 1.5 - send_dtmf 2 - expect(reason).to be_a Punchblock::Component::Input::Complete::Match - end - - it "should cause a NoInput complete event to be sent after the timeout" do - subject.execute - sleep 1.5 - send_dtmf 1 - send_dtmf 2 - expect(reason).to be_a Punchblock::Component::Input::Complete::NoInput - end - end - - context '-1' do - let(:original_command_opts) { { :initial_timeout => -1 } } - - it "should not start a timer" do - expect(subject.wrapped_object).to receive(:begin_initial_timer).never - subject.execute - end - end - - context 'unset' do - let(:original_command_opts) { { :initial_timeout => nil } } - - it "should not start a timer" do - expect(subject.wrapped_object).to receive(:begin_initial_timer).never - subject.execute - end - end - - context 'a negative number other than -1' do - let(:original_command_opts) { { :initial_timeout => -1000 } } - - it "should return an error and not execute any actions" do - subject.execute - error = ProtocolError.new.setup 'option error', 'An initial timeout value that is negative (and not -1) is invalid.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - - describe 'inter-digit-timeout' do - context 'a positive number' do - let(:original_command_opts) { { :inter_digit_timeout => 1000 } } - - it "should not prevent a Match if input is received in time" do - subject.execute - sleep 1.5 - send_dtmf 1 - sleep 0.5 - send_dtmf 2 - expect(reason).to be_a Punchblock::Component::Input::Complete::Match - end - - it "should cause a InterDigitTimeout complete event to be sent after the timeout" do - subject.execute - sleep 1.5 - send_dtmf 1 - sleep 1.5 - send_dtmf 2 - expect(reason).to be_a Punchblock::Component::Input::Complete::NoMatch - end - end - - context '-1' do - let(:original_command_opts) { { :inter_digit_timeout => -1 } } - - it "should not start a timer" do - expect(subject.wrapped_object).to receive(:begin_inter_digit_timer).never - subject.execute - end - end - - context 'unset' do - let(:original_command_opts) { { :inter_digit_timeout => nil } } - - it "should not start a timer" do - expect(subject.wrapped_object).to receive(:begin_inter_digit_timer).never - subject.execute - end - end - - context 'a negative number other than -1' do - let(:original_command_opts) { { :inter_digit_timeout => -1000 } } - - it "should return an error and not execute any actions" do - subject.execute - error = ProtocolError.new.setup 'option error', 'An inter-digit timeout value that is negative (and not -1) is invalid.' - expect(original_command.response(0.1)).to eq(error) - end - end - end - - describe 'sensitivity' do - pending - end - - describe 'min-confidence' do - pending - end - - describe 'max-silence' do - pending - end - end - - describe "#execute_command" do - context "with a command it does not understand" do - let(:command) { Punchblock::Component::Output::Pause.new } - - before { command.request! } - - it "returns a ProtocolError response" do - subject.execute_command command - expect(command.response(0.1)).to be_a ProtocolError - end - end - - context "with a Stop command" do - let(:command) { Punchblock::Component::Stop.new } - let(:reason) { original_command.complete_event(5).reason } - - before do - command.request! - original_command.request! - original_command.execute! - end - - it "sets the command response to true" do - subject.execute_command command - expect(command.response(0.1)).to eq(true) - expect(reason).to be_a Punchblock::Event::Complete::Stop - end - end - end - - end - end - end - end -end diff --git a/spec/punchblock/translator/freeswitch/component/output_spec.rb b/spec/punchblock/translator/freeswitch/component/output_spec.rb deleted file mode 100644 index 7cecf4c4..00000000 --- a/spec/punchblock/translator/freeswitch/component/output_spec.rb +++ /dev/null @@ -1,347 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -module Punchblock - module Translator - class Freeswitch - module Component - describe Output do - include HasMockCallbackConnection - - let(:translator) { Punchblock::Translator::Freeswitch.new connection } - let(:mock_call) { Punchblock::Translator::Freeswitch::Call.new 'foo', translator } - - let :original_command do - Punchblock::Component::Output.new command_options - end - - let :ssml_doc do - RubySpeech::SSML.draw do - say_as(:interpret_as => :cardinal) { 'FOO' } - end - end - - let :command_options do - { :render_document => {:value => ssml_doc} } - end - - subject { Output.new original_command, mock_call } - - describe '#execute' do - before { original_command.request! } - def expect_playback(filename = audio_filename) - expect(subject.wrapped_object).to receive(:application).once.with 'playback', "file_string://#{filename}" - end - - let(:audio_filename) { 'http://foo.com/bar.mp3' } - - let :ssml_doc do - RubySpeech::SSML.draw do - audio :src => audio_filename - end - end - - let(:command_opts) { {} } - - let :command_options do - { :render_document => {:value => ssml_doc} }.merge(command_opts) - end - - let :original_command do - Punchblock::Component::Output.new command_options - end - - describe 'document' do - context 'unset' do - let(:ssml_doc) { nil } - - it "should return an error and not execute any actions" do - subject.execute - error = ProtocolError.new.setup 'option error', 'An SSML document is required.' - expect(original_command.response(0.1)).to eq(error) - end - end - - context 'with a single audio SSML node' do - let(:audio_filename) { 'http://foo.com/bar.mp3' } - let :ssml_doc do - RubySpeech::SSML.draw { audio :src => audio_filename } - end - - it 'should playback the audio file using the playback application' do - expect_playback - subject.execute - end - - it 'should send a complete event when the file finishes playback' do - expect_playback - subject.execute - subject.handle_es_event RubyFS::Event.new(nil, :event_name => "CHANNEL_EXECUTE_COMPLETE", :application_response => 'FILE PLAYED') - expect(original_command.complete_event(0.1).reason).to be_a Punchblock::Component::Output::Complete::Finish - end - - context "when playback returns an error" do - let(:fs_event) { RubyFS::Event.new(nil, :event_name => "CHANNEL_EXECUTE_COMPLETE", :application_response => "PLAYBACK ERROR") } - let(:complete_reason) { original_command.complete_event(0.1).reason } - - it "sends a complete event with an error reason" do - expect_playback - subject.execute - subject.handle_es_event fs_event - expect(complete_reason).to be_a Punchblock::Event::Complete::Error - expect(complete_reason.details).to eq('Engine error: PLAYBACK ERROR') - end - end - end - - context 'with multiple audio SSML nodes' do - let(:audio_filename1) { 'http://foo.com/bar.mp3' } - let(:audio_filename2) { 'http://foo.com/baz.mp3' } - let :ssml_doc do - RubySpeech::SSML.draw do - audio :src => audio_filename1 - audio :src => audio_filename2 - end - end - - it 'should playback all audio files using playback' do - expect_playback [audio_filename1, audio_filename2].join('!') - subject.execute - end - - it 'should send a complete event when the files finish playback' do - expect_playback([audio_filename1, audio_filename2].join('!')) - subject.execute - subject.handle_es_event RubyFS::Event.new(nil, :event_name => "CHANNEL_EXECUTE_COMPLETE", :application_response => "FILE PLAYED") - expect(original_command.complete_event(0.1).reason).to be_a Punchblock::Component::Output::Complete::Finish - end - end - - context "with an SSML document containing elements other than