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.
- data/CHANGELOG.md +87 -0
- data/Guardfile +1 -1
- data/Rakefile +1 -2
- data/adhearsion.gemspec +1 -1
- data/features/cli_create.feature +1 -0
- data/features/step_definitions/cli_steps.rb +0 -10
- data/lib/adhearsion.rb +11 -0
- data/lib/adhearsion/call.rb +27 -12
- data/lib/adhearsion/call_controller.rb +1 -1
- data/lib/adhearsion/call_controller/dial.rb +3 -1
- data/lib/adhearsion/call_controller/input.rb +4 -4
- data/lib/adhearsion/call_controller/menu_dsl.rb +1 -0
- data/lib/adhearsion/call_controller/menu_dsl/array_match_calculator.rb +26 -0
- data/lib/adhearsion/call_controller/menu_dsl/fixnum_match_calculator.rb +2 -14
- data/lib/adhearsion/call_controller/menu_dsl/string_match_calculator.rb +6 -6
- data/lib/adhearsion/call_controller/output.rb +20 -14
- data/lib/adhearsion/call_controller/output/abstract_player.rb +5 -5
- data/lib/adhearsion/call_controller/output/formatter.rb +68 -68
- data/lib/adhearsion/call_controller/record.rb +91 -28
- data/lib/adhearsion/call_controller/utility.rb +12 -5
- data/lib/adhearsion/calls.rb +1 -1
- data/lib/adhearsion/configuration.rb +4 -0
- data/lib/adhearsion/events.rb +2 -1
- data/lib/adhearsion/generators/app/app_generator.rb +2 -2
- data/lib/adhearsion/generators/app/templates/Gemfile.erb +4 -0
- data/lib/adhearsion/generators/app/templates/Rakefile +5 -0
- data/lib/adhearsion/generators/app/templates/lib/simon_game.rb +3 -0
- data/lib/adhearsion/generators/app/templates/rspec +2 -0
- data/lib/adhearsion/generators/app/templates/spec/call_controllers/simon_game_spec.rb +142 -0
- data/lib/adhearsion/generators/controller/templates/lib/controller.rb +1 -1
- data/lib/adhearsion/generators/controller/templates/spec/controller_spec.rb +2 -0
- data/lib/adhearsion/initializer.rb +3 -2
- data/lib/adhearsion/outbound_call.rb +5 -2
- data/lib/adhearsion/router/route.rb +13 -2
- data/lib/adhearsion/rspec.rb +1 -1
- data/lib/adhearsion/statistics.rb +138 -0
- data/lib/adhearsion/tasks/environment.rb +1 -1
- data/lib/adhearsion/version.rb +1 -1
- data/spec/adhearsion/call_controller/dial_spec.rb +26 -0
- data/spec/adhearsion/call_controller/input_spec.rb +13 -1
- data/spec/adhearsion/call_controller/menu_dsl/array_match_calculator_spec.rb +76 -0
- data/spec/adhearsion/call_controller/menu_dsl/fixnum_match_calculator_spec.rb +8 -6
- data/spec/adhearsion/call_controller/menu_dsl/range_match_calculator_spec.rb +4 -4
- data/spec/adhearsion/call_controller/menu_dsl/string_match_calculator_spec.rb +6 -6
- data/spec/adhearsion/call_controller/output/formatter_spec.rb +3 -5
- data/spec/adhearsion/call_controller/output_spec.rb +59 -25
- data/spec/adhearsion/call_controller/record_spec.rb +123 -2
- data/spec/adhearsion/call_controller/utility_spec.rb +31 -11
- data/spec/adhearsion/call_spec.rb +77 -36
- data/spec/adhearsion/calls_spec.rb +13 -0
- data/spec/adhearsion/initializer_spec.rb +7 -0
- data/spec/adhearsion/outbound_call_spec.rb +14 -0
- data/spec/adhearsion/punchblock_plugin_spec.rb +5 -2
- data/spec/adhearsion/router/openended_route_spec.rb +2 -1
- data/spec/adhearsion/router/route_spec.rb +9 -1
- data/spec/adhearsion/router/unaccepting_route_spec.rb +2 -1
- data/spec/adhearsion/statistics/dump_spec.rb +38 -0
- data/spec/adhearsion/statistics_spec.rb +61 -0
- data/spec/adhearsion_spec.rb +21 -1
- data/spec/spec_helper.rb +2 -0
- metadata +16 -7
@@ -38,9 +38,10 @@ module Adhearsion
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def start
|
41
|
+
Adhearsion.statistics
|
41
42
|
resolve_pid_file_path
|
42
43
|
load_lib_folder
|
43
|
-
|
44
|
+
load_config_file
|
44
45
|
initialize_log_paths
|
45
46
|
daemonize! if should_daemonize?
|
46
47
|
start_logging
|
@@ -165,7 +166,7 @@ module Adhearsion
|
|
165
166
|
true
|
166
167
|
end
|
167
168
|
|
168
|
-
def
|
169
|
+
def load_config_file
|
169
170
|
require "#{Adhearsion.config.root}/config/adhearsion.rb"
|
170
171
|
end
|
171
172
|
|
@@ -15,6 +15,7 @@ module Adhearsion
|
|
15
15
|
# @param [String] to the URI of the party to dial
|
16
16
|
# @param [Hash] opts modifier options
|
17
17
|
# @option opts [Class] :controller the controller to execute when the call is answered
|
18
|
+
# @option opts [Hash] :controller_metadata key-value pairs of metadata to set on the controller
|
18
19
|
# @yield Call controller routine in block form
|
19
20
|
#
|
20
21
|
# @return [OutboundCall] the ringing call
|
@@ -23,7 +24,7 @@ module Adhearsion
|
|
23
24
|
#
|
24
25
|
def originate(to, opts = {}, &controller_block)
|
25
26
|
new.tap do |call|
|
26
|
-
call.execute_controller_or_router_on_answer opts.delete(:controller), &controller_block
|
27
|
+
call.execute_controller_or_router_on_answer opts.delete(:controller), opts.delete(:controller_metadata), &controller_block
|
27
28
|
call.dial to, opts
|
28
29
|
end
|
29
30
|
end
|
@@ -69,6 +70,7 @@ module Adhearsion
|
|
69
70
|
write_and_await_response(Punchblock::Command::Dial.new(options), wait_timeout).tap do |dial_command|
|
70
71
|
@dial_command = dial_command
|
71
72
|
Adhearsion.active_calls << current_actor
|
73
|
+
Adhearsion::Events.trigger_immediately :call_dialed, current_actor
|
72
74
|
end
|
73
75
|
end
|
74
76
|
|
@@ -93,9 +95,10 @@ module Adhearsion
|
|
93
95
|
end
|
94
96
|
end
|
95
97
|
|
96
|
-
def execute_controller_or_router_on_answer(controller, &controller_block)
|
98
|
+
def execute_controller_or_router_on_answer(controller, metadata = {}, &controller_block)
|
97
99
|
if controller || controller_block
|
98
100
|
route = Router::Route.new 'inbound', controller, &controller_block
|
101
|
+
route.controller_metadata = metadata
|
99
102
|
on_answer { route.dispatch current_actor }
|
100
103
|
else
|
101
104
|
run_router_on_answer
|
@@ -24,10 +24,12 @@ module Adhearsion
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def dispatch(call, callback = nil)
|
27
|
+
Adhearsion::Events.trigger_immediately :call_routed, call: call, route: self
|
28
|
+
|
27
29
|
controller = if target.respond_to?(:call)
|
28
|
-
CallController.new call, &target
|
30
|
+
CallController.new call, controller_metadata, &target
|
29
31
|
else
|
30
|
-
target.new call
|
32
|
+
target.new call, controller_metadata
|
31
33
|
end
|
32
34
|
|
33
35
|
call.accept if accepting?
|
@@ -45,6 +47,15 @@ module Adhearsion
|
|
45
47
|
}
|
46
48
|
end
|
47
49
|
|
50
|
+
def controller_metadata=(metadata)
|
51
|
+
@controller_metadata = metadata
|
52
|
+
end
|
53
|
+
|
54
|
+
def controller_metadata
|
55
|
+
return {} unless instance_variable_defined?(:@controller_metadata)
|
56
|
+
@controller_metadata
|
57
|
+
end
|
58
|
+
|
48
59
|
def evented?
|
49
60
|
false
|
50
61
|
end
|
data/lib/adhearsion/rspec.rb
CHANGED
@@ -0,0 +1,138 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Adhearsion
|
4
|
+
class Statistics
|
5
|
+
include Celluloid
|
6
|
+
|
7
|
+
exclusive
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@calls_dialed = @calls_offered = @calls_routed = @calls_rejected = 0
|
11
|
+
@calls_by_route = Hash.new { |h,k| h[k] = 0 }
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Create a point-time dump of process statistics
|
16
|
+
#
|
17
|
+
# @return [Adhearsion::Statistics::Dump]
|
18
|
+
def dump
|
19
|
+
Dump.new timestamp: Time.now, call_counts: dump_call_counts, calls_by_route: dump_calls_by_route
|
20
|
+
end
|
21
|
+
|
22
|
+
# @private
|
23
|
+
def register_call_dialed
|
24
|
+
@calls_dialed += 1
|
25
|
+
end
|
26
|
+
|
27
|
+
# @private
|
28
|
+
def register_call_offered
|
29
|
+
@calls_offered += 1
|
30
|
+
end
|
31
|
+
|
32
|
+
# @private
|
33
|
+
def register_call_routed(data)
|
34
|
+
@calls_routed += 1
|
35
|
+
@calls_by_route[data[:route].name] += 1
|
36
|
+
end
|
37
|
+
|
38
|
+
# @private
|
39
|
+
def register_call_rejected
|
40
|
+
@calls_rejected += 1
|
41
|
+
end
|
42
|
+
|
43
|
+
# @private
|
44
|
+
def setup_event_handlers
|
45
|
+
stats = current_actor
|
46
|
+
|
47
|
+
Events.punchblock(Punchblock::Event::Offer) do
|
48
|
+
begin
|
49
|
+
stats.register_call_offered
|
50
|
+
rescue Celluloid::DeadActorError
|
51
|
+
end
|
52
|
+
throw :pass
|
53
|
+
end
|
54
|
+
|
55
|
+
Events.call_dialed do
|
56
|
+
begin
|
57
|
+
stats.register_call_dialed
|
58
|
+
rescue Celluloid::DeadActorError
|
59
|
+
end
|
60
|
+
throw :pass
|
61
|
+
end
|
62
|
+
|
63
|
+
Events.call_rejected do
|
64
|
+
begin
|
65
|
+
stats.register_call_rejected
|
66
|
+
rescue Celluloid::DeadActorError
|
67
|
+
end
|
68
|
+
throw :pass
|
69
|
+
end
|
70
|
+
|
71
|
+
Events.call_routed do |data|
|
72
|
+
begin
|
73
|
+
stats.register_call_routed data
|
74
|
+
rescue Celluloid::DeadActorError
|
75
|
+
end
|
76
|
+
throw :pass
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_s
|
81
|
+
"#<#{self.class} dump=#{dump}>"
|
82
|
+
end
|
83
|
+
alias :inspect :to_s
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def dump_call_counts
|
88
|
+
{dialed: @calls_dialed, offered: @calls_offered, routed: @calls_routed, rejected: @calls_rejected, active: Adhearsion.active_calls.count}
|
89
|
+
end
|
90
|
+
|
91
|
+
def dump_calls_by_route
|
92
|
+
@calls_by_route.tap do |index|
|
93
|
+
Adhearsion.router.routes.each do |route|
|
94
|
+
index[route.name]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# A point-time dump of process statistics
|
101
|
+
class Dump
|
102
|
+
include Comparable
|
103
|
+
|
104
|
+
#
|
105
|
+
# @attribute
|
106
|
+
# @return [Time] the time at which this dump was generated
|
107
|
+
attr_reader :timestamp
|
108
|
+
|
109
|
+
#
|
110
|
+
# @attribute
|
111
|
+
# @return [Hash] hash of call counts during the lifetime of the process.
|
112
|
+
attr_reader :call_counts
|
113
|
+
|
114
|
+
#
|
115
|
+
# @attribute
|
116
|
+
# @return [Hash] hash of call counts during the lifetime of the process, indexed by the route they matched.
|
117
|
+
attr_reader :calls_by_route
|
118
|
+
|
119
|
+
def initialize(opts = {})
|
120
|
+
@timestamp = opts[:timestamp]
|
121
|
+
@call_counts = opts[:call_counts]
|
122
|
+
@calls_by_route = opts[:calls_by_route]
|
123
|
+
end
|
124
|
+
|
125
|
+
def <=>(other)
|
126
|
+
timestamp <=> other.timestamp
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_s
|
130
|
+
attrs = [:timestamp, :call_counts, :calls_by_route].map do |attr|
|
131
|
+
"#{attr}=#{send(attr).inspect}"
|
132
|
+
end
|
133
|
+
"#<#{self.class} #{attrs.join ', '}>"
|
134
|
+
end
|
135
|
+
alias :inspect :to_s
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -7,7 +7,7 @@ task :environment do
|
|
7
7
|
Adhearsion.config # load default config vlaues
|
8
8
|
initializer = Adhearsion::Initializer.new
|
9
9
|
initializer.load_lib_folder
|
10
|
-
initializer.
|
10
|
+
initializer.load_config_file
|
11
11
|
rescue Exception => ex
|
12
12
|
STDERR.puts "\nError while loading application configuration file: #{ex}"
|
13
13
|
end
|
data/lib/adhearsion/version.rb
CHANGED
@@ -436,6 +436,10 @@ module Adhearsion
|
|
436
436
|
@@confirmation_latch = latch
|
437
437
|
|
438
438
|
def run
|
439
|
+
# Copy metadata onto call variables so we can assert it later. Ugly hack
|
440
|
+
metadata.each_pair do |key, value|
|
441
|
+
call[key] = value
|
442
|
+
end
|
439
443
|
@@confirmation_latch.countdown!
|
440
444
|
call['confirm'] || hangup
|
441
445
|
end
|
@@ -451,6 +455,28 @@ module Adhearsion
|
|
451
455
|
flexmock(OutboundCall).should_receive(:new).and_return other_mock_call
|
452
456
|
end
|
453
457
|
|
458
|
+
context "with confirmation controller metadata specified" do
|
459
|
+
let(:options) { {:confirm => confirmation_controller, :confirm_metadata => {:foo => 'bar'}} }
|
460
|
+
|
461
|
+
it "should set the metadata on the controller" do
|
462
|
+
flexmock(other_mock_call).should_receive(:hangup).twice.and_return do
|
463
|
+
other_mock_call << mock_end
|
464
|
+
end
|
465
|
+
other_mock_call['confirm'] = false
|
466
|
+
|
467
|
+
dial_in_thread
|
468
|
+
|
469
|
+
latch.wait(0.1).should be_false
|
470
|
+
|
471
|
+
other_mock_call << mock_answered
|
472
|
+
|
473
|
+
confirmation_latch.wait(1).should be_true
|
474
|
+
latch.wait(2).should be_true
|
475
|
+
|
476
|
+
other_mock_call[:foo].should == 'bar'
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
454
480
|
context "when an outbound call is answered" do
|
455
481
|
it "should execute the specified confirmation controller" do
|
456
482
|
flexmock(other_mock_call).should_receive(:hangup).twice.and_return do
|
@@ -63,9 +63,11 @@ module Adhearsion
|
|
63
63
|
)
|
64
64
|
}
|
65
65
|
|
66
|
+
let(:utterance) { 'dtmf-5' }
|
67
|
+
|
66
68
|
def expect_component_complete_event
|
67
69
|
complete_event = Punchblock::Event::Complete.new
|
68
|
-
flexmock(complete_event).should_receive(:reason => flexmock(:
|
70
|
+
flexmock(complete_event).should_receive(:reason => flexmock(:utterance => utterance, :name => :input))
|
69
71
|
flexmock(Punchblock::Component::Input).new_instances do |input|
|
70
72
|
input.should_receive(:complete?).and_return(false)
|
71
73
|
input.should_receive(:complete_event).and_return(complete_event)
|
@@ -84,6 +86,16 @@ module Adhearsion
|
|
84
86
|
subject.wait_for_digit(timeout).should be == '5'
|
85
87
|
end
|
86
88
|
|
89
|
+
context "when the utterance does not have dtmf- prefix" do
|
90
|
+
let(:utterance) { '5' }
|
91
|
+
|
92
|
+
it "returns the correct pressed digit" do
|
93
|
+
expect_component_complete_event
|
94
|
+
subject.should_receive(:execute_component_and_await_completion).once.with(Punchblock::Component::Input).and_return input_component
|
95
|
+
subject.wait_for_digit(timeout).should be == '5'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
87
99
|
context "with a nil timeout" do
|
88
100
|
let(:timeout) { nil }
|
89
101
|
let(:timeout_ms) { nil }
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module Adhearsion
|
6
|
+
class CallController
|
7
|
+
module MenuDSL
|
8
|
+
describe ArrayMatchCalculator do
|
9
|
+
|
10
|
+
let(:match_payload) { :doesnt_matter }
|
11
|
+
|
12
|
+
it "matching arrays with fixnums" do
|
13
|
+
calculator = ArrayMatchCalculator.new [11,5,14,115], match_payload
|
14
|
+
match_case = calculator.match '11'
|
15
|
+
match_case.should be_exact_match
|
16
|
+
match_case.should be_potential_match
|
17
|
+
match_case.exact_matches.should be == [11]
|
18
|
+
match_case.potential_matches.should be == [115]
|
19
|
+
end
|
20
|
+
|
21
|
+
it "matching arrays with strings with digits and special digits" do
|
22
|
+
calculator = ArrayMatchCalculator.new %w[*57 4 *54 115 ###], match_payload
|
23
|
+
match_case = calculator.match '*5'
|
24
|
+
match_case.should_not be_exact_match
|
25
|
+
match_case.should be_potential_match
|
26
|
+
match_case.potential_matches.should be == %w[*57 *54]
|
27
|
+
|
28
|
+
match_case = calculator.match '*57'
|
29
|
+
match_case.should be_exact_match
|
30
|
+
match_case.should_not be_potential_match
|
31
|
+
match_case.exact_matches.should be == %w[*57]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "matching an array with a combination of Fixnums and Strings" do
|
35
|
+
calculator = ArrayMatchCalculator.new ['11',5,'14',115], match_payload
|
36
|
+
match_case = calculator.match '11'
|
37
|
+
match_case.should be_exact_match
|
38
|
+
match_case.should be_potential_match
|
39
|
+
match_case.exact_matches.should be == ['11']
|
40
|
+
match_case.potential_matches.should be == [115]
|
41
|
+
end
|
42
|
+
|
43
|
+
it "matching empty array should never match" do
|
44
|
+
calculator = ArrayMatchCalculator.new [], match_payload
|
45
|
+
match_case = calculator.match '98'
|
46
|
+
match_case.should_not be_exact_match
|
47
|
+
match_case.should_not be_potential_match
|
48
|
+
match_case.exact_matches.should be == []
|
49
|
+
match_case.potential_matches.should be == []
|
50
|
+
|
51
|
+
match_case = calculator.match '*2'
|
52
|
+
match_case.should_not be_exact_match
|
53
|
+
match_case.should_not be_potential_match
|
54
|
+
match_case.exact_matches.should be == []
|
55
|
+
match_case.potential_matches.should be == []
|
56
|
+
end
|
57
|
+
|
58
|
+
it "matching array with nil should skip nil field" do
|
59
|
+
pattern = [1,2,nil,5,10]
|
60
|
+
calculator = ArrayMatchCalculator.new pattern, match_payload
|
61
|
+
match_case = calculator.match '1'
|
62
|
+
match_case.should be_exact_match
|
63
|
+
match_case.should be_potential_match
|
64
|
+
match_case.exact_matches.should be == [1]
|
65
|
+
match_case.potential_matches.should be == [10]
|
66
|
+
|
67
|
+
match_case = calculator.match '99'
|
68
|
+
match_case.should_not be_exact_match
|
69
|
+
match_case.should_not be_potential_match
|
70
|
+
|
71
|
+
pattern.should == [1,2,nil,5,10]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -10,26 +10,28 @@ module Adhearsion
|
|
10
10
|
|
11
11
|
it "a potential match scenario" do
|
12
12
|
calculator = FixnumMatchCalculator.new(444, match_payload)
|
13
|
-
match = calculator.match 4
|
14
|
-
match.
|
15
|
-
match.
|
13
|
+
match = calculator.match '4'
|
14
|
+
match.should be_potential_match
|
15
|
+
match.should_not be_exact_match
|
16
16
|
match.potential_matches.should be == [444]
|
17
17
|
end
|
18
18
|
|
19
19
|
it "a multi-digit exact match scenario" do
|
20
20
|
calculator = FixnumMatchCalculator.new(5555, match_payload)
|
21
|
-
calculator.match
|
21
|
+
match = calculator.match '5555'
|
22
|
+
match.should be_exact_match
|
22
23
|
end
|
23
24
|
|
24
25
|
it "a single-digit exact match scenario" do
|
25
26
|
calculator = FixnumMatchCalculator.new(1, match_payload)
|
26
|
-
calculator.match
|
27
|
+
match = calculator.match '1'
|
28
|
+
match.should be_exact_match
|
27
29
|
end
|
28
30
|
|
29
31
|
it "the context name given to the calculator should be passed on the CalculatedMatch" do
|
30
32
|
match_payload = :icanhascheezburger
|
31
33
|
calculator = FixnumMatchCalculator.new(1337, match_payload)
|
32
|
-
calculator.match(1337).match_payload.should be match_payload
|
34
|
+
calculator.match('1337').match_payload.should be match_payload
|
33
35
|
end
|
34
36
|
end
|
35
37
|
end
|