adhearsion 2.1.3 → 2.2.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 (61) hide show
  1. data/CHANGELOG.md +87 -0
  2. data/Guardfile +1 -1
  3. data/Rakefile +1 -2
  4. data/adhearsion.gemspec +1 -1
  5. data/features/cli_create.feature +1 -0
  6. data/features/step_definitions/cli_steps.rb +0 -10
  7. data/lib/adhearsion.rb +11 -0
  8. data/lib/adhearsion/call.rb +27 -12
  9. data/lib/adhearsion/call_controller.rb +1 -1
  10. data/lib/adhearsion/call_controller/dial.rb +3 -1
  11. data/lib/adhearsion/call_controller/input.rb +4 -4
  12. data/lib/adhearsion/call_controller/menu_dsl.rb +1 -0
  13. data/lib/adhearsion/call_controller/menu_dsl/array_match_calculator.rb +26 -0
  14. data/lib/adhearsion/call_controller/menu_dsl/fixnum_match_calculator.rb +2 -14
  15. data/lib/adhearsion/call_controller/menu_dsl/string_match_calculator.rb +6 -6
  16. data/lib/adhearsion/call_controller/output.rb +20 -14
  17. data/lib/adhearsion/call_controller/output/abstract_player.rb +5 -5
  18. data/lib/adhearsion/call_controller/output/formatter.rb +68 -68
  19. data/lib/adhearsion/call_controller/record.rb +91 -28
  20. data/lib/adhearsion/call_controller/utility.rb +12 -5
  21. data/lib/adhearsion/calls.rb +1 -1
  22. data/lib/adhearsion/configuration.rb +4 -0
  23. data/lib/adhearsion/events.rb +2 -1
  24. data/lib/adhearsion/generators/app/app_generator.rb +2 -2
  25. data/lib/adhearsion/generators/app/templates/Gemfile.erb +4 -0
  26. data/lib/adhearsion/generators/app/templates/Rakefile +5 -0
  27. data/lib/adhearsion/generators/app/templates/lib/simon_game.rb +3 -0
  28. data/lib/adhearsion/generators/app/templates/rspec +2 -0
  29. data/lib/adhearsion/generators/app/templates/spec/call_controllers/simon_game_spec.rb +142 -0
  30. data/lib/adhearsion/generators/controller/templates/lib/controller.rb +1 -1
  31. data/lib/adhearsion/generators/controller/templates/spec/controller_spec.rb +2 -0
  32. data/lib/adhearsion/initializer.rb +3 -2
  33. data/lib/adhearsion/outbound_call.rb +5 -2
  34. data/lib/adhearsion/router/route.rb +13 -2
  35. data/lib/adhearsion/rspec.rb +1 -1
  36. data/lib/adhearsion/statistics.rb +138 -0
  37. data/lib/adhearsion/tasks/environment.rb +1 -1
  38. data/lib/adhearsion/version.rb +1 -1
  39. data/spec/adhearsion/call_controller/dial_spec.rb +26 -0
  40. data/spec/adhearsion/call_controller/input_spec.rb +13 -1
  41. data/spec/adhearsion/call_controller/menu_dsl/array_match_calculator_spec.rb +76 -0
  42. data/spec/adhearsion/call_controller/menu_dsl/fixnum_match_calculator_spec.rb +8 -6
  43. data/spec/adhearsion/call_controller/menu_dsl/range_match_calculator_spec.rb +4 -4
  44. data/spec/adhearsion/call_controller/menu_dsl/string_match_calculator_spec.rb +6 -6
  45. data/spec/adhearsion/call_controller/output/formatter_spec.rb +3 -5
  46. data/spec/adhearsion/call_controller/output_spec.rb +59 -25
  47. data/spec/adhearsion/call_controller/record_spec.rb +123 -2
  48. data/spec/adhearsion/call_controller/utility_spec.rb +31 -11
  49. data/spec/adhearsion/call_spec.rb +77 -36
  50. data/spec/adhearsion/calls_spec.rb +13 -0
  51. data/spec/adhearsion/initializer_spec.rb +7 -0
  52. data/spec/adhearsion/outbound_call_spec.rb +14 -0
  53. data/spec/adhearsion/punchblock_plugin_spec.rb +5 -2
  54. data/spec/adhearsion/router/openended_route_spec.rb +2 -1
  55. data/spec/adhearsion/router/route_spec.rb +9 -1
  56. data/spec/adhearsion/router/unaccepting_route_spec.rb +2 -1
  57. data/spec/adhearsion/statistics/dump_spec.rb +38 -0
  58. data/spec/adhearsion/statistics_spec.rb +61 -0
  59. data/spec/adhearsion_spec.rb +21 -1
  60. data/spec/spec_helper.rb +2 -0
  61. metadata +16 -7
@@ -17,12 +17,12 @@ module Adhearsion
17
17
  end
18
18
  end
19
19
 
20
- def play_ssml_for(*args)
21
- play_ssml Formatter.ssml_for(args)
22
- end
23
-
24
20
  def new_output(options)
25
- Punchblock::Component::Output.new options
21
+ defaults = {}
22
+ default_voice = Adhearsion.config.punchblock[:default_voice]
23
+ defaults[:voice] = default_voice if default_voice
24
+
25
+ Punchblock::Component::Output.new defaults.merge(options)
26
26
  end
27
27
  end
28
28
  end
@@ -3,90 +3,90 @@
3
3
  module Adhearsion
4
4
  class CallController
5
5
  module Output
6
- module Formatter
6
+ class Formatter
7
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
- case output
24
- when Date, Time, DateTime
25
- :time
26
- when Numeric, /^\d+$/
27
- :numeric
28
- when /^\//, ->(string) { uri? string }
29
- :audio
8
+ def ssml_for_collection(collection)
9
+ collection.inject RubySpeech::SSML::Speak.new do |doc, argument|
10
+ doc + case argument
11
+ when Hash
12
+ ssml_for argument.delete(:value), argument
13
+ when RubySpeech::SSML::Speak
14
+ argument
15
+ when lambda { |a| a.respond_to? :each }
16
+ ssml_for_collection argument
30
17
  else
31
- :text
18
+ ssml_for argument
32
19
  end
33
20
  end
21
+ end
34
22
 
35
- def uri?(string)
36
- uri = URI.parse string
37
- !!uri.scheme
38
- rescue URI::BadURIError
39
- false
40
- rescue URI::InvalidURIError
41
- false
23
+ def detect_type(output)
24
+ case output
25
+ when Date, Time, DateTime
26
+ :time
27
+ when Numeric, /^\d+$/
28
+ :numeric
29
+ when /^\//, ->(string) { uri? string }
30
+ :audio
31
+ else
32
+ :text
42
33
  end
34
+ end
43
35
 
44
- #
45
- # Generates SSML for the argument and options passed, using automatic detection
46
- # Directly returns the argument if it is already an SSML document
47
- #
48
- # @param [String, Hash, RubySpeech::SSML::Speak] the argument with options as accepted by the play_ methods, or an SSML document
49
- # @return [RubySpeech::SSML::Speak] an SSML document
50
- #
51
- def ssml_for(*args)
52
- return args[0] if args.size == 1 && args[0].is_a?(RubySpeech::SSML::Speak)
53
- argument, options = args.flatten
54
- options ||= {}
55
- type = detect_type argument
56
- send "ssml_for_#{type}", argument, options
57
- end
36
+ def uri?(string)
37
+ uri = URI.parse string
38
+ !!uri.scheme
39
+ rescue URI::BadURIError
40
+ false
41
+ rescue URI::InvalidURIError
42
+ false
43
+ end
58
44
 
59
- def ssml_for_text(argument, options = {})
60
- RubySpeech::SSML.draw { argument }
61
- end
45
+ #
46
+ # Generates SSML for the argument and options passed, using automatic detection
47
+ # Directly returns the argument if it is already an SSML document
48
+ #
49
+ # @param [String, Hash, RubySpeech::SSML::Speak] the argument with options as accepted by the play_ methods, or an SSML document
50
+ # @return [RubySpeech::SSML::Speak] an SSML document
51
+ #
52
+ def ssml_for(*args)
53
+ return args[0] if args.size == 1 && args[0].is_a?(RubySpeech::SSML::Speak)
54
+ argument, options = args.flatten
55
+ options ||= {}
56
+ type = detect_type argument
57
+ send "ssml_for_#{type}", argument, options
58
+ end
62
59
 
63
- def ssml_for_time(argument, options = {})
64
- interpretation = case argument
65
- when Date then 'date'
66
- when Time then 'time'
67
- end
60
+ def ssml_for_text(argument, options = {})
61
+ RubySpeech::SSML.draw { argument }
62
+ end
68
63
 
69
- format = options.delete :format
70
- strftime = options.delete :strftime
64
+ def ssml_for_time(argument, options = {})
65
+ interpretation = case argument
66
+ when Date then 'date'
67
+ when Time then 'time'
68
+ end
71
69
 
72
- time_to_say = strftime ? argument.strftime(strftime) : argument.to_s
70
+ format = options.delete :format
71
+ strftime = options.delete :strftime
73
72
 
74
- RubySpeech::SSML.draw do
75
- say_as(:interpret_as => interpretation, :format => format) { time_to_say }
76
- end
73
+ time_to_say = strftime ? argument.strftime(strftime) : argument.to_s
74
+
75
+ RubySpeech::SSML.draw do
76
+ say_as(:interpret_as => interpretation, :format => format) { time_to_say }
77
77
  end
78
+ end
78
79
 
79
- def ssml_for_numeric(argument, options = {})
80
- RubySpeech::SSML.draw do
81
- say_as(:interpret_as => 'cardinal') { argument.to_s }
82
- end
80
+ def ssml_for_numeric(argument, options = {})
81
+ RubySpeech::SSML.draw do
82
+ say_as(:interpret_as => 'cardinal') { argument.to_s }
83
83
  end
84
+ end
84
85
 
85
- def ssml_for_audio(argument, options = {})
86
- fallback = (options || {}).delete :fallback
87
- RubySpeech::SSML.draw do
88
- audio(:src => argument) { fallback }
89
- end
86
+ def ssml_for_audio(argument, options = {})
87
+ fallback = (options || {}).delete :fallback
88
+ RubySpeech::SSML.draw do
89
+ audio(:src => argument) { fallback }
90
90
  end
91
91
  end
92
92
  end
@@ -5,6 +5,93 @@ module Adhearsion
5
5
  module Record
6
6
  RecordError = Class.new StandardError # Represents failure to record such as when a file cannot be written.
7
7
 
8
+ #
9
+ # Handle a recording
10
+ #
11
+ # @param [Adhearsion::CallController] controller on which to execute the recording
12
+ # @param [Hash] options
13
+ # @option options [Boolean, Optional] :async Execute asynchronously. Defaults to false
14
+ # @option options [Boolean, Optional] :start_beep Indicates whether subsequent record will be preceded with a beep. Default is true.
15
+ # @option options [Boolean, Optional] :start_paused Whether subsequent record will start in PAUSE mode. Default is false.
16
+ # @option options [String, Optional] :max_duration Indicates the maximum duration (seconds) for a recording.
17
+ # @option options [String, Optional] :format File format used during recording.
18
+ # @option options [String, Optional] :initial_timeout Controls how long (seconds) the recognizer should wait after the end of the prompt for the caller to speak before sending a Recorder event.
19
+ # @option options [String, Optional] :final_timeout Controls the length (seconds) of a period of silence after callers have spoken to conclude they finished.
20
+ # @option options [Boolean, Optional] :interruptible Allows the recording to be terminated by any single DTMF key, default is false
21
+ #
22
+ class Recorder
23
+ attr_accessor :record_component, :stopper_component
24
+
25
+ def initialize(controller, options = {})
26
+ @controller = controller
27
+
28
+ options = prep_options options
29
+
30
+ @async = options.delete :async
31
+
32
+ @stopper_component = options.delete(:interruptible) ? setup_stopper : nil
33
+ @record_component = Punchblock::Component::Record.new options
34
+ end
35
+
36
+ #
37
+ # Execute the recorder
38
+ #
39
+ # @return nil
40
+ #
41
+ def run
42
+ execute_stopper
43
+ execute_recording
44
+ terminate_stopper
45
+ nil
46
+ end
47
+
48
+ #
49
+ # Set a callback to be executed when recording completes
50
+ #
51
+ # @yield [Punchblock::Event::Complete] the complete Event for the recording
52
+ #
53
+ def handle_record_completion(&block)
54
+ @record_component.register_event_handler Punchblock::Event::Complete, &block
55
+ end
56
+
57
+ private
58
+
59
+ def setup_stopper
60
+ @stopper_component = Punchblock::Component::Input.new :mode => :dtmf,
61
+ :grammar => {
62
+ :value => @controller.grammar_accept('0123456789#*')
63
+ }
64
+ @stopper_component.register_event_handler Punchblock::Event::Complete do |event|
65
+ @record_component.stop! unless @record_component.complete?
66
+ end
67
+ @stopper_component
68
+ end
69
+
70
+ def execute_stopper
71
+ @controller.write_and_await_response @stopper_component if @stopper_component
72
+ end
73
+
74
+ def execute_recording
75
+ if @async
76
+ @controller.write_and_await_response @record_component
77
+ else
78
+ @controller.execute_component_and_await_completion @record_component
79
+ end
80
+ end
81
+
82
+ def terminate_stopper
83
+ @stopper_component.stop! if @stopper_component && !@stopper_component.complete?
84
+ end
85
+
86
+ def prep_options(opts)
87
+ opts.dup.tap do |options|
88
+ [:max_duration, :initial_timeout, :final_timeout].each do |k|
89
+ options[k] = options[k] * 1000 if options[k]
90
+ end
91
+ end
92
+ end
93
+ end
94
+
8
95
  #
9
96
  # Start a recording
10
97
  #
@@ -23,7 +110,6 @@ module Adhearsion
23
110
  # @option options [Boolean, Optional] :start_paused Whether subsequent record will start in PAUSE mode. Default is false.
24
111
  # @option options [String, Optional] :max_duration Indicates the maximum duration (seconds) for a recording.
25
112
  # @option options [String, Optional] :format File format used during recording.
26
- # @option options [String, Optional] :format File format used during recording.
27
113
  # @option options [String, Optional] :initial_timeout Controls how long (seconds) the recognizer should wait after the end of the prompt for the caller to speak before sending a Recorder event.
28
114
  # @option options [String, Optional] :final_timeout Controls the length (seconds) of a period of silence after callers have spoken to conclude they finished.
29
115
  # @option options [Boolean, Optional] :interruptible Allows the recording to be terminated by any single DTMF key, default is false
@@ -31,37 +117,14 @@ module Adhearsion
31
117
  # @return Punchblock::Component::Record
32
118
  #
33
119
  def record(options = {})
34
- async = options.delete :async
35
- interruptible = options.delete :interruptible
36
- interrupt_key = '0123456789#*'
37
- stopper_component = nil
38
- [:max_duration, :initial_timeout, :final_timeout].each do |k|
39
- options[k] = options[k].to_i * 1000 if options[k]
40
- end
120
+ recorder = Recorder.new self, options
41
121
 
42
- component = Punchblock::Component::Record.new options
43
- component.register_event_handler Punchblock::Event::Complete do |event|
122
+ recorder.handle_record_completion do |event|
44
123
  catching_standard_errors { yield event if block_given? }
45
124
  end
46
125
 
47
- if interruptible
48
- stopper_component = Punchblock::Component::Input.new :mode => :dtmf,
49
- :grammar => {
50
- :value => grammar_accept(interrupt_key)
51
- }
52
- stopper_component.register_event_handler Punchblock::Event::Complete do |event|
53
- component.stop! unless component.complete?
54
- end
55
- write_and_await_response stopper_component
56
- end
57
-
58
- if async
59
- write_and_await_response component
60
- else
61
- execute_component_and_await_completion component
62
- end
63
- stopper_component.stop! if stopper_component && stopper_component.executing?
64
- component
126
+ recorder.run
127
+ recorder.record_component
65
128
  end
66
129
  end
67
130
  end
@@ -44,16 +44,23 @@ module Adhearsion
44
44
  end
45
45
 
46
46
  #
47
- # Parses a single DTMF tone in the format dtmf-*
47
+ # Parses a DTMF tone string
48
48
  #
49
49
  # @param [String] the tone string to be parsed
50
- # @return [String] the digit in case input was 0-9, * or # if star or pound respectively
50
+ # @return [String] the digits/*/# without any separation
51
51
  #
52
52
  # @private
53
53
  #
54
- def parse_single_dtmf(result)
55
- return if result.nil?
56
- case tone = result.split('-')[1]
54
+ def parse_dtmf(dtmf)
55
+ return if dtmf.nil?
56
+ dtmf.split(' ').inject '' do |final, digit|
57
+ final << parse_dtmf_digit(digit)
58
+ end
59
+ end
60
+
61
+ # @private
62
+ def parse_dtmf_digit(digit)
63
+ case tone = digit.split('-').last
57
64
  when 'star'
58
65
  '*'
59
66
  when 'pound'
@@ -45,10 +45,10 @@ module Adhearsion
45
45
  end
46
46
 
47
47
  def call_died(call, reason)
48
- return unless reason
49
48
  catching_standard_errors do
50
49
  call_id = key call
51
50
  remove_inactive_call call
51
+ return unless reason
52
52
  PunchblockPlugin.client.execute_command Punchblock::Command::Hangup.new, :async => true, :call_id => call_id
53
53
  end
54
54
  end
@@ -52,6 +52,10 @@ module Adhearsion
52
52
  A log formatter to apply to all active outputters. If nil, the Adhearsion default formatter will be used.
53
53
  __
54
54
  }
55
+
56
+ after_hangup_lifetime 30, :transform => Proc.new { |v| v.to_i }, :desc => <<-__
57
+ Lifetime of a call after it has hung up
58
+ __
55
59
  end
56
60
 
57
61
  Loquacious::Configuration.for :platform, &block if block_given?
@@ -83,7 +83,8 @@ module Adhearsion
83
83
  private
84
84
 
85
85
  def call_handler(handler, guards, event)
86
- super && throw(:pass)
86
+ super
87
+ throw :pass
87
88
  end
88
89
 
89
90
  class ErrorHandler
@@ -4,8 +4,8 @@ module Adhearsion
4
4
  module Generators
5
5
  class AppGenerator < Generator
6
6
 
7
- BASEDIRS = %w( config lib script spec )
8
- EMPTYDIRS = %w( spec/call_controllers spec/support )
7
+ BASEDIRS = %w( config lib script spec )
8
+ EMPTYDIRS = %w( spec/support )
9
9
 
10
10
  def setup_project
11
11
  self.destination_root = @generator_name
@@ -13,3 +13,7 @@ gem "adhearsion", "~> <%= Adhearsion::VERSION.split('.')[0,2].join('.') %>"
13
13
  # gem 'adhearsion-ldap'
14
14
  # gem 'adhearsion-xmpp'
15
15
  # gem 'adhearsion-drb'
16
+
17
+ group :development, :test do
18
+ gem 'rspec'
19
+ end
@@ -3,3 +3,8 @@
3
3
  require File.expand_path('../config/environment', __FILE__)
4
4
 
5
5
  require 'adhearsion/tasks'
6
+
7
+ task :default => :spec
8
+
9
+ require 'rspec/core/rake_task'
10
+ RSpec::Core::RakeTask.new(:spec)
@@ -1,6 +1,9 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  class SimonGame < Adhearsion::CallController
4
+
5
+ attr_accessor :number, :attempt
6
+
4
7
  def run
5
8
  answer
6
9
  reset
@@ -1 +1,3 @@
1
+ --format documentation
1
2
  --colour
3
+ --tty
@@ -0,0 +1,142 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe SimonGame do
6
+
7
+ let(:example_response) { OpenStruct.new(:response => "5") }
8
+ let(:example_number) { "5" }
9
+ let(:long_response) { OpenStruct.new(:response => "55555") }
10
+ let(:long_number) { "55555" }
11
+
12
+ let(:mock_call) { mock 'Call' }
13
+ subject { SimonGame.new(mock_call) }
14
+
15
+ describe "#random_number" do
16
+
17
+ before { subject.stub!(:rand).and_return(example_number) }
18
+
19
+ it "generates a random number" do
20
+ subject.random_number.should eq example_number
21
+ end
22
+ end
23
+
24
+ describe "#update_number" do
25
+
26
+ before { subject.number = "123" }
27
+ before { subject.stub!(:random_number).and_return "4" }
28
+
29
+ it "adds a digit to the end of the number" do
30
+ subject.update_number
31
+ subject.number.should eq "1234"
32
+ end
33
+ end
34
+
35
+ describe "#collect_attempt" do
36
+
37
+ context "when the @number is 1 digit long" do
38
+
39
+ before { subject.number = "3" }
40
+
41
+ it "asks for a 1 digits number" do
42
+ subject.should_receive(:ask).with("3", :limit => 1).and_return(example_response)
43
+ subject.collect_attempt
44
+ end
45
+ end
46
+
47
+ context "when the @number is 5 digits long" do
48
+
49
+ before { subject.number = long_number }
50
+
51
+ it "asks for a 5 digits number" do
52
+ subject.should_receive(:ask).with(long_number, :limit => 5).and_return(long_response)
53
+ subject.collect_attempt
54
+ end
55
+ end
56
+
57
+ context "sets @attempt" do
58
+
59
+ before { subject.number = "12345" }
60
+
61
+ it "based on the user's response" do
62
+ subject.should_receive(:ask).with("12345", :limit => 5).and_return(long_response)
63
+ subject.collect_attempt
64
+ subject.attempt.should eq long_number
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "#attempt_correct?" do
70
+
71
+ before { subject.number = "7" }
72
+
73
+ context "with a good attempt" do
74
+
75
+ before { subject.attempt = "7" }
76
+
77
+ it "returns true" do
78
+ subject.attempt_correct?.should be_true
79
+ end
80
+ end
81
+
82
+ context "with a bad attempt" do
83
+
84
+ before { subject.attempt = "9" }
85
+
86
+ it "returns true" do
87
+ subject.attempt_correct?.should be_false
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "#verify_attempt" do
93
+ context "when the user is a good guesser" do
94
+
95
+ before { subject.stub!(:attempt_correct?).and_return true }
96
+
97
+ it "congradulates them" do
98
+ subject.should_receive(:speak).with('good')
99
+ subject.verify_attempt
100
+ end
101
+ end
102
+
103
+ context "when the user guesses wrong" do
104
+
105
+ before { subject.number = "12345" }
106
+ before { subject.attempt = "12346" }
107
+
108
+ it "congradulates them" do
109
+ subject.should_receive(:speak).with('4 times wrong, try again smarty')
110
+ subject.verify_attempt
111
+ end
112
+ end
113
+ end
114
+
115
+ describe "#reset" do
116
+
117
+ before { subject.reset }
118
+
119
+ it "sets @number" do
120
+ subject.number.should eq ''
121
+ end
122
+
123
+ it "sets @attempt" do
124
+ subject.attempt.should eq ''
125
+ end
126
+ end
127
+
128
+ describe "#run" do
129
+ it "loops the loop" do
130
+ subject.should_receive :answer
131
+ subject.should_receive :reset
132
+
133
+ subject.should_receive :update_number
134
+ subject.should_receive :collect_attempt
135
+ subject.should_receive(:verify_attempt).and_throw :rspec_loop_stop
136
+
137
+ catch :rspec_loop_stop do
138
+ subject.run
139
+ end
140
+ end
141
+ end
142
+ end