adhearsion 2.1.3 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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