adhearsion 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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