adhearsion 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.travis.yml +4 -3
  2. data/CHANGELOG.md +30 -0
  3. data/README.markdown +1 -0
  4. data/adhearsion.gemspec +3 -4
  5. data/bin/ahn +0 -20
  6. data/features/cli_create.feature +1 -1
  7. data/features/cli_restart.feature +25 -1
  8. data/features/cli_start.feature +0 -2
  9. data/features/plugin_generator.feature +66 -15
  10. data/features/support/env.rb +0 -13
  11. data/lib/adhearsion.rb +26 -6
  12. data/lib/adhearsion/call.rb +42 -7
  13. data/lib/adhearsion/call_controller.rb +5 -2
  14. data/lib/adhearsion/call_controller/dial.rb +92 -50
  15. data/lib/adhearsion/call_controller/input.rb +19 -6
  16. data/lib/adhearsion/call_controller/menu_dsl/menu.rb +4 -0
  17. data/lib/adhearsion/call_controller/output.rb +143 -161
  18. data/lib/adhearsion/call_controller/output/abstract_player.rb +30 -0
  19. data/lib/adhearsion/call_controller/output/async_player.rb +26 -0
  20. data/lib/adhearsion/call_controller/output/formatter.rb +81 -0
  21. data/lib/adhearsion/call_controller/output/player.rb +25 -0
  22. data/lib/adhearsion/call_controller/record.rb +19 -2
  23. data/lib/adhearsion/events.rb +3 -0
  24. data/lib/adhearsion/foundation.rb +12 -6
  25. data/lib/adhearsion/foundation/exception_handler.rb +8 -6
  26. data/lib/adhearsion/generators/app/templates/README.md +13 -0
  27. data/lib/adhearsion/generators/app/templates/config/adhearsion.rb +7 -1
  28. data/lib/adhearsion/generators/plugin/plugin_generator.rb +1 -0
  29. data/lib/adhearsion/generators/plugin/templates/plugin-template.gemspec.tt +3 -7
  30. data/lib/adhearsion/generators/plugin/templates/spec/spec_helper.rb.tt +0 -1
  31. data/lib/adhearsion/outbound_call.rb +15 -5
  32. data/lib/adhearsion/punchblock_plugin.rb +13 -2
  33. data/lib/adhearsion/punchblock_plugin/initializer.rb +13 -12
  34. data/lib/adhearsion/router.rb +43 -2
  35. data/lib/adhearsion/router/evented_route.rb +15 -0
  36. data/lib/adhearsion/router/openended_route.rb +16 -0
  37. data/lib/adhearsion/router/route.rb +31 -13
  38. data/lib/adhearsion/router/unaccepting_route.rb +11 -0
  39. data/lib/adhearsion/version.rb +1 -1
  40. data/pre-commit +14 -1
  41. data/spec/adhearsion/call_controller/dial_spec.rb +105 -10
  42. data/spec/adhearsion/call_controller/input_spec.rb +19 -21
  43. data/spec/adhearsion/call_controller/output/async_player_spec.rb +67 -0
  44. data/spec/adhearsion/call_controller/output/formatter_spec.rb +90 -0
  45. data/spec/adhearsion/call_controller/output/player_spec.rb +65 -0
  46. data/spec/adhearsion/call_controller/output_spec.rb +436 -190
  47. data/spec/adhearsion/call_controller/record_spec.rb +49 -6
  48. data/spec/adhearsion/call_controller_spec.rb +10 -2
  49. data/spec/adhearsion/call_spec.rb +138 -0
  50. data/spec/adhearsion/calls_spec.rb +1 -1
  51. data/spec/adhearsion/outbound_call_spec.rb +48 -8
  52. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +34 -23
  53. data/spec/adhearsion/router/evented_route_spec.rb +34 -0
  54. data/spec/adhearsion/router/openended_route_spec.rb +61 -0
  55. data/spec/adhearsion/router/route_spec.rb +26 -4
  56. data/spec/adhearsion/router/unaccepting_route_spec.rb +72 -0
  57. data/spec/adhearsion/router_spec.rb +107 -2
  58. data/spec/adhearsion_spec.rb +19 -0
  59. data/spec/capture_warnings.rb +28 -21
  60. data/spec/spec_helper.rb +2 -3
  61. data/spec/support/call_controller_test_helpers.rb +31 -30
  62. metadata +32 -29
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ module Adhearsion
4
+ class CallController
5
+ module Output
6
+ class AbstractPlayer
7
+
8
+ attr_accessor :controller
9
+
10
+ def initialize(controller)
11
+ @controller = controller
12
+ end
13
+
14
+ def play_ssml(ssml, options = {})
15
+ if [RubySpeech::SSML::Speak, Nokogiri::XML::Document].include? ssml.class
16
+ output ssml, options
17
+ end
18
+ end
19
+
20
+ def play_ssml_for(*args)
21
+ play_ssml Formatter.ssml_for(args)
22
+ end
23
+
24
+ def new_output(options)
25
+ Punchblock::Component::Output.new options
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ module Adhearsion
4
+ class CallController
5
+ module Output
6
+ class AsyncPlayer < AbstractPlayer
7
+
8
+ #
9
+ # @yields The output component before executing it
10
+ # @raises [PlaybackError] if (one of) the given argument(s) could not be played
11
+ #
12
+ def output(content, options = {})
13
+ options.merge! :ssml => content.to_s
14
+ component = new_output options
15
+ component.register_event_handler Punchblock::Event::Complete do |event|
16
+ controller.logger.error event if event.reason.is_a?(Punchblock::Event::Complete::Error)
17
+ end
18
+ controller.write_and_await_response component
19
+ component
20
+ rescue Punchblock::ProtocolError => e
21
+ raise PlaybackError, "Async output failed due to #{e.inspect}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,81 @@
1
+ # encoding: utf-8
2
+
3
+ module Adhearsion
4
+ class CallController
5
+ module Output
6
+ module Formatter
7
+
8
+ class << self
9
+ def ssml_for_collection(collection)
10
+ collection.inject RubySpeech::SSML::Speak.new do |doc, argument|
11
+ doc + case argument
12
+ when Hash
13
+ Formatter.ssml_for argument.delete(:value), argument
14
+ when RubySpeech::SSML::Speak
15
+ argument
16
+ else
17
+ Formatter.ssml_for argument
18
+ end
19
+ end
20
+ end
21
+
22
+ def detect_type(output)
23
+ result = nil
24
+ result = :time if [Date, Time, DateTime].include? output.class
25
+ result = :numeric if output.kind_of?(Numeric) || output =~ /^\d+$/
26
+ result = :audio if !result && (/^\//.match(output.to_s) || URI::regexp.match(output.to_s))
27
+ result ||= :text
28
+ end
29
+
30
+ #
31
+ # Generates SSML for the argument and options passed, using automatic detection
32
+ # Directly returns the argument if it is already an SSML document
33
+ #
34
+ # @param [String, Hash, RubySpeech::SSML::Speak] the argument with options as accepted by the play_ methods, or an SSML document
35
+ # @return [RubySpeech::SSML::Speak] an SSML document
36
+ #
37
+ def ssml_for(*args)
38
+ return args[0] if args.size == 1 && args[0].is_a?(RubySpeech::SSML::Speak)
39
+ argument, options = args.flatten
40
+ options ||= {}
41
+ type = detect_type argument
42
+ send "ssml_for_#{type}", argument, options
43
+ end
44
+
45
+ def ssml_for_text(argument, options = {})
46
+ RubySpeech::SSML.draw { argument }
47
+ end
48
+
49
+ def ssml_for_time(argument, options = {})
50
+ interpretation = case argument
51
+ when Date then 'date'
52
+ when Time then 'time'
53
+ end
54
+
55
+ format = options.delete :format
56
+ strftime = options.delete :strftime
57
+
58
+ time_to_say = strftime ? argument.strftime(strftime) : argument.to_s
59
+
60
+ RubySpeech::SSML.draw do
61
+ say_as(:interpret_as => interpretation, :format => format) { time_to_say }
62
+ end
63
+ end
64
+
65
+ def ssml_for_numeric(argument, options = {})
66
+ RubySpeech::SSML.draw do
67
+ say_as(:interpret_as => 'cardinal') { argument.to_s }
68
+ end
69
+ end
70
+
71
+ def ssml_for_audio(argument, options = {})
72
+ fallback = (options || {}).delete :fallback
73
+ RubySpeech::SSML.draw do
74
+ audio(:src => argument) { fallback }
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ module Adhearsion
4
+ class CallController
5
+ module Output
6
+ class Player < AbstractPlayer
7
+
8
+ #
9
+ # @yields The output component before executing it
10
+ # @raises [PlaybackError] if (one of) the given argument(s) could not be played
11
+ #
12
+ def output(content, options = {})
13
+ options.merge! :ssml => content.to_s
14
+ component = new_output options
15
+ yield component if block_given?
16
+ controller.execute_component_and_await_completion component
17
+ rescue Call::Hangup
18
+ raise
19
+ rescue Adhearsion::Error, Punchblock::ProtocolError => e
20
+ raise PlaybackError, "Output failed due to #{e.inspect}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -26,22 +26,39 @@ module Adhearsion
26
26
  # @option options [String, Optional] :format File format used during recording.
27
27
  # @option options [String, Optional] :initial_timeout Controls how long (milliseconds) the recognizer should wait after the end of the prompt for the caller to speak before sending a Recorder event.
28
28
  # @option options [String, Optional] :final_timeout Controls the length (milliseconds) of a period of silence after callers have spoken to conclude they finished.
29
+ # @option options [Boolean, Optional] :interruptible Allows the recording to be terminated by any single DTMF key, default is false
29
30
  #
30
31
  # @return Punchblock::Component::Record
31
32
  #
32
33
  def record(options = {})
33
34
  async = options.delete :async
35
+ interruptible = options.delete :interruptible
36
+ interrupt_key = '0123456789#*'
37
+ stopper_component = nil
34
38
 
35
- component = ::Punchblock::Component::Record.new options
36
- component.register_event_handler ::Punchblock::Event::Complete do |event|
39
+ component = Punchblock::Component::Record.new options
40
+ component.register_event_handler Punchblock::Event::Complete do |event|
37
41
  catching_standard_errors { yield event if block_given? }
38
42
  end
39
43
 
44
+ if interruptible
45
+ stopper_component = Punchblock::Component::Input.new :mode => :dtmf,
46
+ :grammar => {
47
+ :value => grammar_accept(interrupt_key)
48
+ }
49
+ stopper_component.register_event_handler Punchblock::Event::Complete do |event|
50
+ component.stop! unless component.complete?
51
+ end
52
+ write_and_await_response stopper_component
53
+ end
54
+
40
55
  if async
41
56
  write_and_await_response component
42
57
  else
43
58
  execute_component_and_await_completion component
44
59
  end
60
+ stopper_component.stop! if stopper_component && stopper_component.executing?
61
+ component
45
62
  end
46
63
  end
47
64
  end
@@ -1,5 +1,8 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'has_guarded_handlers'
4
+ require 'girl_friday'
5
+
3
6
  module Adhearsion
4
7
  class Events
5
8
 
@@ -1,9 +1,15 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'English'
4
- require 'tmpdir'
5
- require 'tempfile'
3
+ %w{
4
+ English
5
+ tmpdir
6
+ tempfile
7
+ }.each { |f| require f }
6
8
 
7
- Dir.glob File.join(File.dirname(__FILE__), 'foundation', "*rb") do |file|
8
- require file
9
- end
9
+ %w{
10
+ custom_daemonizer
11
+ exception_handler
12
+ libc
13
+ object
14
+ thread_safety
15
+ }.each { |f| require "adhearsion/foundation/#{f}" }
@@ -1,11 +1,13 @@
1
1
  # encoding: utf-8
2
2
 
3
- class Object
3
+ module Adhearsion::Safely
4
4
  def catching_standard_errors(l = logger, &block)
5
- begin
6
- yield
7
- rescue StandardError => e
8
- Adhearsion::Events.trigger :exception, [e, l]
9
- end
5
+ yield
6
+ rescue StandardError => e
7
+ Adhearsion::Events.trigger :exception, [e, l]
10
8
  end
11
9
  end
10
+
11
+ class Object
12
+ include Adhearsion::Safely
13
+ end
@@ -15,6 +15,19 @@ and setup a user in `manager.conf` with read/write access to `all`.
15
15
 
16
16
  If you are using Asterisk 1.8, you will need to add an additional context with the name `adhearsion-redirect`. On Asterisk 10 and above this is auto-provisioned.
17
17
 
18
+ ## FreeSWITCH
19
+
20
+ * Ensure that mod_event_socket is installed, and configure it in autoload_configs/event_socket.conf.xml to taste
21
+ * Add an extension to your dialplan like so:
22
+
23
+ ```xml
24
+ <extension name='Adhearsion'>
25
+ <condition field="destination_number" expression="^10$">
26
+ <action application='park'/>
27
+ </condition>
28
+ </extension>
29
+ ```
30
+
18
31
  ## Voxeo PRISM
19
32
 
20
33
  Install the [rayo-server](https://github.com/rayo/rayo-server) app into PRISM 11 and follow the [configuration guide](https://github.com/rayo/rayo-server/wiki/Single-node-and-cluster-configuration-reference).
@@ -34,7 +34,13 @@ Adhearsion.config do |config|
34
34
  # config.punchblock.username = "" # Your AMI username
35
35
  # config.punchblock.password = "" # Your AMI password
36
36
  # config.punchblock.host = "127.0.0.1" # Your AMI host
37
- # config.punchblock.port = 5038 # Your AMI port
37
+
38
+ ##
39
+ # Use with FreeSWITCH
40
+ #
41
+ # config.punchblock.platform = :freeswitch # Use FreeSWITCH
42
+ # config.punchblock.password = "" # Your Inbound EventSocket password
43
+ # config.punchblock.host = "127.0.0.1" # Your IES host
38
44
  end
39
45
 
40
46
  Adhearsion::Events.draw do
@@ -12,6 +12,7 @@ module Adhearsion
12
12
 
13
13
  def create_plugin
14
14
  @plugin_file = @plugin_name.underscore
15
+ @plugin_name = @plugin_name.camelize
15
16
  self.destination_root = '.'
16
17
 
17
18
  empty_directory @plugin_file
@@ -20,15 +20,11 @@ Gem::Specification.new do |s|
20
20
  s.test_files = Dir.glob("{spec}/**/*")
21
21
  s.require_paths = ["lib"]
22
22
 
23
- s.add_runtime_dependency %q<adhearsion>, [">= <%= Adhearsion::VERSION %>"]
23
+ s.add_runtime_dependency %q<adhearsion>, ["~> <%= Adhearsion::VERSION.split('.')[0..1].join('.') %>"]
24
24
  s.add_runtime_dependency %q<activesupport>, [">= 3.0.10"]
25
25
 
26
- s.add_development_dependency %q<bundler>, ["~> 1.0.0"]
27
- s.add_development_dependency %q<rspec>, [">= 2.5.0"]
28
- s.add_development_dependency %q<ci_reporter>, [">= 1.6.3"]
29
- s.add_development_dependency %q<simplecov>, [">= 0"]
30
- s.add_development_dependency %q<simplecov-rcov>, [">= 0"]
31
- s.add_development_dependency %q<yard>, ["~> 0.6.0"]
26
+ s.add_development_dependency %q<bundler>, ["~> 1.0"]
27
+ s.add_development_dependency %q<rspec>, ["~> 2.5"]
32
28
  s.add_development_dependency %q<rake>, [">= 0"]
33
29
  s.add_development_dependency %q<mocha>, [">= 0"]
34
30
  s.add_development_dependency %q<guard-rspec>
@@ -1,5 +1,4 @@
1
1
  require 'adhearsion'
2
- require 'mocha'
3
2
  require '<%= @plugin_file %>'
4
3
 
5
4
  RSpec.configure do |config|
@@ -7,9 +7,9 @@ module Adhearsion
7
7
  delegate :to, :from, :to => :dial_command, :allow_nil => true
8
8
 
9
9
  class << self
10
- def originate(to, opts = {})
10
+ def originate(to, opts = {}, &controller_block)
11
11
  new.tap do |call|
12
- call.run_router_on_answer
12
+ call.execute_controller_or_router_on_answer opts.delete(:controller), &controller_block
13
13
  call.dial to, opts
14
14
  end
15
15
  end
@@ -33,7 +33,8 @@ module Adhearsion
33
33
  end
34
34
 
35
35
  def dial(to, options = {})
36
- options.merge! :to => to
36
+ options = options.dup
37
+ options[:to] = to
37
38
  if options[:timeout]
38
39
  wait_timeout = options[:timeout]
39
40
  options[:timeout] = options[:timeout] * 1000
@@ -55,17 +56,26 @@ module Adhearsion
55
56
  end
56
57
 
57
58
  def run_router_on_answer
58
- register_event_handler :class => Punchblock::Event::Answered do |event|
59
+ register_event_handler Punchblock::Event::Answered do |event|
59
60
  run_router
60
61
  throw :pass
61
62
  end
62
63
  end
63
64
 
64
65
  def on_answer(&block)
65
- register_event_handler :class => Punchblock::Event::Answered do |event|
66
+ register_event_handler Punchblock::Event::Answered do |event|
66
67
  block.call event
67
68
  throw :pass
68
69
  end
69
70
  end
71
+
72
+ def execute_controller_or_router_on_answer(controller, &controller_block)
73
+ if controller || controller_block
74
+ route = Router::Route.new 'inbound', controller, &controller_block
75
+ on_answer { route.dispatch current_actor }
76
+ else
77
+ run_router_on_answer
78
+ end
79
+ end
70
80
  end
71
81
  end
@@ -11,11 +11,12 @@ module Adhearsion
11
11
  Platform punchblock shall use to connect to the Telephony provider. Currently supported values:
12
12
  - :xmpp
13
13
  - :asterisk
14
+ - :freeswitch
14
15
  __
15
16
  username "usera@127.0.0.1", :desc => "Authentication credentials"
16
17
  password "1" , :desc => "Authentication credentials"
17
- host nil , :desc => "Host punchblock needs to connect (where rayo or asterisk are located)"
18
- port nil , :transform => Proc.new { |v| PunchblockPlugin.validate_number v }, :desc => "Port punchblock needs to connect (by default 5038 for Asterisk, 5222 for Rayo)"
18
+ host nil , :desc => "Host punchblock needs to connect (where rayo/asterisk/freeswitch is located)"
19
+ port Proc.new { PunchblockPlugin.default_port_for_platform platform }, :transform => Proc.new { |v| PunchblockPlugin.validate_number v }, :desc => "Port punchblock needs to connect"
19
20
  root_domain nil , :desc => "The root domain at which to address the server"
20
21
  calls_domain nil , :desc => "The domain at which to address calls"
21
22
  mixers_domain nil , :desc => "The domain at which to address mixers"
@@ -23,6 +24,7 @@ module Adhearsion
23
24
  reconnect_attempts 1.0/0.0 , :transform => Proc.new { |v| PunchblockPlugin.validate_number v }, :desc => "The number of times to (re)attempt connection to the server"
24
25
  reconnect_timer 5 , :transform => Proc.new { |v| PunchblockPlugin.validate_number v }, :desc => "Delay between connection attempts"
25
26
  media_engine nil , :transform => Proc.new { |v| v.to_sym }, :desc => "The media engine to use. Defaults to platform default."
27
+ default_voice nil , :transform => Proc.new { |v| v.to_sym }, :desc => "The default TTS voice to use."
26
28
  end
27
29
 
28
30
  init :punchblock do
@@ -42,6 +44,15 @@ module Adhearsion
42
44
  value.to_i
43
45
  end
44
46
 
47
+ def default_port_for_platform(platform)
48
+ case platform
49
+ when :freeswitch then 8021
50
+ when :asterisk then 5038
51
+ when :xmpp then 5222
52
+ else nil
53
+ end
54
+ end
55
+
45
56
  def execute_component(command, timeout = 60)
46
57
  client.execute_command command, :async => true
47
58
  response = command.response timeout
@@ -19,9 +19,11 @@ module Adhearsion
19
19
  username = Blather::JID.new username
20
20
  username = Blather::JID.new username.node, username.domain, resource unless username.resource
21
21
  username = username.to_s
22
- ::Punchblock::Connection::XMPP
22
+ Punchblock::Connection::XMPP
23
23
  when :asterisk
24
- ::Punchblock::Connection::Asterisk
24
+ Punchblock::Connection::Asterisk
25
+ when :freeswitch
26
+ Punchblock::Connection::Freeswitch
25
27
  end
26
28
 
27
29
  connection_options = {
@@ -33,11 +35,12 @@ module Adhearsion
33
35
  :root_domain => self.config.root_domain,
34
36
  :calls_domain => self.config.calls_domain,
35
37
  :mixers_domain => self.config.mixers_domain,
36
- :media_engine => self.config.media_engine
38
+ :media_engine => self.config.media_engine,
39
+ :default_voice => self.config.default_voice
37
40
  }
38
41
 
39
42
  self.connection = connection_class.new connection_options
40
- self.client = ::Punchblock::Client.new :connection => connection
43
+ self.client = Punchblock::Client.new :connection => connection
41
44
 
42
45
  # Tell the Punchblock connection that we are ready to process calls.
43
46
  Events.register_callback :after_initialization do
@@ -60,12 +63,12 @@ module Adhearsion
60
63
  handle_event event
61
64
  end
62
65
 
63
- Events.punchblock ::Punchblock::Connection::Connected do |event|
66
+ Events.punchblock Punchblock::Connection::Connected do |event|
64
67
  logger.info "Connected to Punchblock server"
65
68
  self.attempts = 0
66
69
  end
67
70
 
68
- Events.punchblock ::Punchblock::Event::Offer do |offer|
71
+ Events.punchblock Punchblock::Event::Offer do |offer|
69
72
  dispatch_offer offer
70
73
  end
71
74
 
@@ -87,7 +90,7 @@ module Adhearsion
87
90
  m = Mutex.new
88
91
  blocker = ConditionVariable.new
89
92
 
90
- Events.punchblock ::Punchblock::Connection::Connected do
93
+ Events.punchblock Punchblock::Connection::Connected do
91
94
  Adhearsion::Process.booted
92
95
  m.synchronize { blocker.broadcast }
93
96
  end
@@ -109,7 +112,7 @@ module Adhearsion
109
112
  begin
110
113
  logger.info "Starting connection to server"
111
114
  client.run
112
- rescue ::Punchblock::DisconnectedError => e
115
+ rescue Punchblock::DisconnectedError => e
113
116
  # We only care about disconnects if the process is up or booting
114
117
  return unless [:booting, :running].include? Adhearsion::Process.state_name
115
118
 
@@ -126,7 +129,7 @@ module Adhearsion
126
129
  logger.error "Connection lost. Attempting reconnect #{self.attempts} of #{self.config.reconnect_attempts}"
127
130
  sleep self.config.reconnect_timer
128
131
  retry
129
- rescue ::Punchblock::ProtocolError => e
132
+ rescue Punchblock::ProtocolError => e
130
133
  logger.fatal "The connection failed due to a protocol error: #{e.name}."
131
134
  raise e
132
135
  end
@@ -140,9 +143,7 @@ module Adhearsion
140
143
  logger.info "Declining call because the process is not yet running."
141
144
  call.reject :decline
142
145
  when :running
143
- call.accept
144
- dispatcher = Adhearsion.router.handle call
145
- dispatcher.call call
146
+ Adhearsion.router.handle call
146
147
  else
147
148
  call.reject :error
148
149
  end