electric_slide 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +10 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +1 -1
- data/README.markdown +33 -6
- data/electric_slide.gemspec +3 -1
- data/lib/electric_slide.rb +36 -36
- data/lib/electric_slide/agent.rb +61 -38
- data/lib/electric_slide/agent_strategy/longest_idle.rb +1 -1
- data/lib/electric_slide/call_queue.rb +172 -38
- data/lib/electric_slide/version.rb +1 -1
- data/spec/electric_slide/agent_spec.rb +29 -3
- data/spec/electric_slide/agent_strategy/fixed_priority_spec.rb +7 -8
- data/spec/electric_slide/call_queue_spec.rb +436 -25
- data/spec/electric_slide_spec.rb +58 -14
- data/spec/spec_helper.rb +6 -0
- metadata +23 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 018120f529e0824da4b7fb13d5ce713b53c16ec7
|
4
|
+
data.tar.gz: 7172bc885ad750962da497dd0d1afa560f5d5729
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b3f3a9fd91292a2582f60c53f1d27a924b9ecb43d66013140f9c41ade3708fc2f6793a06cb3300bff0a4641116b1a9cef8afdcca84a120e3eff9fdf52043b1f
|
7
|
+
data.tar.gz: 8d9a3b56211a774348a0f7b953308415a69ae662e40355a63782786d9ce525350d9662ab4bec9be55edefb33bdab4fda8ff5d8248ba467125d6161ac2ba3dda6
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# [develop](https://github.com/adhearsion/electric_slide)
|
2
|
+
* Trigger an agent "presence change" callback when the agent is removed from the queue
|
3
|
+
* Bugfix: Fix `NameError` exception when referencing namespaced constant `Adhearsion::Call::ExpiredError` in injected `Adhearsion::Call#active?` method
|
4
|
+
* Provide Electric Slide with a way to check if a queue with a given set of attributes is valid/can be instantiated before Electric Slide adds it to the supervision group. The supervisor will crash if its attempt to create the queue raises an exception.
|
5
|
+
* Add support for changing queue attributes
|
6
|
+
* API Breakage: Setting queue connection type to an invalid value will now raise an `ElectricSlide::CallQueue::InvalidConnectionType` exception instead of `ArgumentError`
|
7
|
+
* API Breakage: Setting queue agent return method to an invalid value will now raise an `ElectricSlide::CallQueue::InvalidRequeueMethod` exception instead of `ArgumentError`
|
8
|
+
* List created queues by name via `ElectricSlide.queues_by_name`
|
9
|
+
* Set `:agent` call variable on queued call when connecting calls
|
10
|
+
* API Breakage: Queues must now be Celluloid actors responding to the standard actor API. `ElectricSlide::CallQueue.work` is removed in favour of `.new`.
|
11
|
+
* API Breakage: Prevent an unqueued agent from being returned to the queue - this avoids calls after logging out
|
12
|
+
* API Breakage: An agent's presence should be :after_call if they are not automatically returned to being available
|
13
|
+
* API Breakage: Store queued/connected timestamps on calls
|
14
|
+
* API Breakage: Remove abandoned calls from the queue
|
15
|
+
* Set agent `#call` attribute to outbound call made to agent in :call mode
|
16
|
+
* Prevent an agent from being added to the queue more than once
|
17
|
+
* Added Travis CI configuration
|
18
|
+
* Lots more test coverage - still bad
|
19
|
+
|
20
|
+
# [0.2.0](https://github.com/adhearsion/electric_slide/compare/bb3b1b3e7f6d0926d0a9f462520e1f6d0c277adf...v0.2.0) - [2015-07-23](https://rubygems.org/gems/adhearsion/versions/0.2.0)
|
21
|
+
* ¯\\_(ツ)_/¯
|
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2011 The Adhearsion Foundation, Inc.
|
1
|
+
Copyright (c) 2011-2015 The Adhearsion Foundation, Inc.
|
2
2
|
|
3
3
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
4
|
|
data/README.markdown
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Electric Slide - Simple Call Distribution for Adhearsion
|
2
2
|
====================================================================
|
3
3
|
|
4
|
-
This library implements a simple
|
4
|
+
This library implements a simple call queue for Adhearsion.
|
5
5
|
|
6
6
|
To ensure proper operation, a few things are assumed:
|
7
7
|
|
@@ -63,7 +63,7 @@ end
|
|
63
63
|
Adding an Agent to the Queue
|
64
64
|
----------------------------
|
65
65
|
|
66
|
-
ElectricSlide expects to be
|
66
|
+
ElectricSlide expects to be instances of the `ElectricSlide::Agent` class. This is designed to be extended with custom functionality when necessary.
|
67
67
|
|
68
68
|
To add an agent who will receive calls whenever a call is enqueued, do something like this:
|
69
69
|
|
@@ -72,7 +72,7 @@ agent = ElectricSlide::Agent.new id: 1, address: 'sip:agent1@example.com', prese
|
|
72
72
|
ElectricSlide.get_queue(:my_queue).add_agent agent
|
73
73
|
```
|
74
74
|
|
75
|
-
To inform the queue that the agent is no longer available you *must* use the ElectricSlide queue interface.
|
75
|
+
To inform the queue that the agent is no longer available you *must* use the ElectricSlide queue interface. **_Do not attempt to change agent objects directly!_**
|
76
76
|
|
77
77
|
```ruby
|
78
78
|
ElectricSlide.update_agent 1, presence: offline
|
@@ -84,12 +84,21 @@ If it is more convenient, you may also pass `#update_agent` an Agent-like object
|
|
84
84
|
options = {
|
85
85
|
id: 1,
|
86
86
|
address: 'sip:agent1@example.com',
|
87
|
-
presence:
|
87
|
+
presence: :unavailable
|
88
88
|
}
|
89
89
|
agent = ElectricSlide::Agent.new options
|
90
90
|
ElectricSlide.update_agent 1, agent
|
91
91
|
```
|
92
92
|
|
93
|
+
The possible presence states for an Agent are:
|
94
|
+
|
95
|
+
* `:available` - Waiting for a call
|
96
|
+
* `:on_call` - Currently connected to a call
|
97
|
+
* `:after_call` - In a quiet period after completing a call and before being made available again. This is only encountered with a manual agent return strategy.
|
98
|
+
* `:unavailable` - Agent is not available (on break, offline, etc)
|
99
|
+
|
100
|
+
Note that an `:unavailable` agent still counts as an agent in the queue, but will not be sent any calls. Make sure to remove agents, even unavailable ones, when agents sign out by using the `#remove_agent` method.
|
101
|
+
|
93
102
|
Switching connection types
|
94
103
|
--------------------------
|
95
104
|
|
@@ -124,8 +133,10 @@ Custom Agent Behavior
|
|
124
133
|
|
125
134
|
If you need custom functionality to occur whenever an Agent is selected to take a call, you can use the callbacks on the Agent object:
|
126
135
|
|
127
|
-
* `on_connect
|
128
|
-
* `on_disconnect
|
136
|
+
* `on_connect`: Args: [Queue, Agent Call, Client Call] Called as the agent is being connected to the client call
|
137
|
+
* `on_disconnect`: Args: [Queue, Agent Call, Client Call] Called after the agent is disconnected from the client for any reason (eg. hangup)
|
138
|
+
* `connection_failed`: Args: [Queue, Agent Call, Client Call] Called when the agent fails to connect with the client for any reason (eg. no answer)
|
139
|
+
* `presence_change`: Args: [Queue, Agent Call, New Presence] Called after the agent's presence changes
|
129
140
|
|
130
141
|
Confirmation Controllers
|
131
142
|
------------------------
|
@@ -151,3 +162,19 @@ call.join metadata[:caller] if confirm!
|
|
151
162
|
```
|
152
163
|
|
153
164
|
where `confirm!` is your logic for deciding if you want the call to be connected or not. Hanging up during the confirmation controller or letting it finish without any action will result in the call being sent to the next agent.
|
165
|
+
|
166
|
+
Credits
|
167
|
+
-------
|
168
|
+
|
169
|
+
Electric Slide Copyright 2011-2015 Adhearsion Foundation Inc.
|
170
|
+
See the LICENSE file for more information.
|
171
|
+
|
172
|
+
Original Author [Ben Klang](https://github.com/bklang) - Mojo Lingo
|
173
|
+
|
174
|
+
Contributors:
|
175
|
+
* [Ben Langfeld](https://github.com/benlangfeld) - Mojo Lingo
|
176
|
+
* [Neil Decapia](https://github.com/neildecapia) - Mojo Lingo
|
177
|
+
* [Lloyd Hughes](https://github.com/system123) - Teleforge
|
178
|
+
* [Luca Pradovera](https://github.com/polysics) - Mojo Lingo
|
179
|
+
|
180
|
+
Also thanks to [Power Home Remodeling Group](http://powerhrg.com), [Teleforge](http://teleforge.co.za), and Atlanta Game Adventures for sponsoring development.
|
data/electric_slide.gemspec
CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
|
|
18
18
|
|
19
19
|
s.has_rdoc = true
|
20
20
|
s.homepage = "http://github.com/adhearsion/electric_slide"
|
21
|
+
s.license = "MIT"
|
21
22
|
s.require_paths = ["lib"]
|
22
23
|
s.rubygems_version = "1.2.0"
|
23
24
|
s.summary = "Automatic Call Distributor for Adhearsion"
|
@@ -25,7 +26,8 @@ Gem::Specification.new do |s|
|
|
25
26
|
s.add_runtime_dependency 'adhearsion'
|
26
27
|
s.add_runtime_dependency 'countdownlatch'
|
27
28
|
s.add_runtime_dependency 'activesupport'
|
28
|
-
s.add_development_dependency 'rspec', ['
|
29
|
+
s.add_development_dependency 'rspec', ['~> 3.0']
|
30
|
+
s.add_development_dependency 'timecop'
|
29
31
|
s.add_development_dependency 'ci_reporter'
|
30
32
|
s.add_development_dependency 'guard'
|
31
33
|
s.add_development_dependency 'guard-rspec'
|
data/lib/electric_slide.rb
CHANGED
@@ -1,68 +1,68 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'celluloid'
|
3
|
-
require 'singleton'
|
4
3
|
|
5
4
|
require 'adhearsion/version'
|
6
5
|
|
7
6
|
if Gem::Version.new(Adhearsion::VERSION) < Gem::Version.new('3.0.0')
|
8
7
|
# Backport https://github.com/adhearsion/adhearsion/commit/8c6855612c70dd822fb4e4c2006d1fdc9d05fe23 to avoid confusion around dead calls
|
9
8
|
require 'adhearsion/call'
|
10
|
-
class Adhearsion::Call
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
class Adhearsion::Call
|
10
|
+
class ActorProxy
|
11
|
+
def active?
|
12
|
+
alive? && super
|
13
|
+
rescue ExpiredError
|
14
|
+
false
|
15
|
+
end
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
18
19
|
|
19
20
|
%w(
|
21
|
+
agent
|
20
22
|
call_queue
|
21
23
|
plugin
|
22
24
|
).each { |f| require "electric_slide/#{f}" }
|
23
25
|
|
24
26
|
class ElectricSlide
|
25
|
-
|
27
|
+
class Supervisor < Celluloid::SupervisionGroup
|
28
|
+
def [](name)
|
29
|
+
@registry[name]
|
30
|
+
end
|
26
31
|
|
27
|
-
|
28
|
-
|
29
|
-
|
32
|
+
def names
|
33
|
+
@registry.names
|
34
|
+
end
|
30
35
|
end
|
31
36
|
|
32
|
-
|
33
|
-
fail "Queue with name #{name} already exists!" if @queues.key? name
|
37
|
+
@supervisor = Supervisor.run!(Celluloid::Registry.new)
|
34
38
|
|
35
|
-
|
36
|
-
@
|
37
|
-
|
38
|
-
|
39
|
+
def self.queues_by_name
|
40
|
+
@supervisor.names.inject({}) do |queues, name|
|
41
|
+
queues[name] = get_queue(name)
|
42
|
+
queues
|
43
|
+
end
|
39
44
|
end
|
40
45
|
|
41
|
-
def
|
42
|
-
fail "Queue #{name}
|
43
|
-
get_queue name
|
44
|
-
end
|
46
|
+
def self.create(name, queue_class = nil, *args)
|
47
|
+
fail "Queue with name #{name} already exists!" if get_queue(name)
|
45
48
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
queue.actors.first
|
51
|
-
else
|
52
|
-
queue
|
49
|
+
queue_class ||= CallQueue
|
50
|
+
if !queue_class.respond_to?(:valid_with?) || queue_class.valid_with?(*args)
|
51
|
+
@supervisor.supervise_as name, (queue_class || CallQueue), *args
|
52
|
+
get_queue name
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
-
def
|
57
|
-
|
58
|
-
queue.terminate
|
59
|
-
@queues.delete name
|
56
|
+
def self.get_queue!(name)
|
57
|
+
get_queue(name) || fail("Queue #{name} not found!")
|
60
58
|
end
|
61
59
|
|
62
|
-
def self.
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
60
|
+
def self.get_queue(name)
|
61
|
+
@supervisor[name]
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.shutdown_queue(name)
|
65
|
+
queue = get_queue name
|
66
|
+
queue.terminate if queue
|
67
67
|
end
|
68
68
|
end
|
data/lib/electric_slide/agent.rb
CHANGED
@@ -1,48 +1,71 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
class ElectricSlide::Agent
|
3
|
-
attr_accessor :id, :address, :presence, :call, :connect_callback, :disconnect_callback
|
4
|
-
|
5
|
-
# @param [Hash] opts Agent parameters
|
6
|
-
# @option opts [String] :id The Agent's ID
|
7
|
-
# @option opts [String] :address The Agent's contact address
|
8
|
-
# @option opts [Symbol] :presence The Agent's current presence. Must be one of :available, :on_call, :away, :offline
|
9
|
-
def initialize(opts = {})
|
10
|
-
@id = opts[:id]
|
11
|
-
@address = opts[:address]
|
12
|
-
@presence = opts[:presence]
|
13
|
-
end
|
14
2
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
3
|
+
class ElectricSlide
|
4
|
+
class Agent
|
5
|
+
attr_accessor :id, :address, :presence, :call, :queue
|
19
6
|
|
7
|
+
# @param [Hash] opts Agent parameters
|
8
|
+
# @option opts [String] :id The Agent's ID
|
9
|
+
# @option opts [String] :address The Agent's contact address
|
10
|
+
# @option opts [Symbol] :presence The Agent's current presence. Must be one of :available, :on_call, :after_call, :unavailable
|
11
|
+
def initialize(opts = {})
|
12
|
+
@id = opts[:id]
|
13
|
+
@address = opts[:address]
|
14
|
+
@presence = opts[:presence] || :available
|
15
|
+
end
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
17
|
+
def presence=(new_presence)
|
18
|
+
old_presence = @presence
|
19
|
+
@presence = new_presence
|
20
|
+
callback :presence_change, queue, @call, new_presence, old_presence
|
21
|
+
end
|
26
22
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
23
|
+
def callback(type, *args)
|
24
|
+
callback = self.class.instance_variable_get "@#{type}_callback"
|
25
|
+
instance_exec *args, &callback if callback && callback.respond_to?(:call)
|
26
|
+
end
|
32
27
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
28
|
+
# Provide a block to be called when this agent is connected to a caller
|
29
|
+
# The block will be passed the queue, the agent call and the client call
|
30
|
+
def self.on_connect(&block)
|
31
|
+
@connect_callback = block
|
32
|
+
end
|
37
33
|
|
38
|
-
|
39
|
-
#
|
40
|
-
|
41
|
-
|
34
|
+
# Provide a block to be called when this agent is disconnected to a caller
|
35
|
+
# The block will be passed the queue, the agent call and the client call
|
36
|
+
def self.on_disconnect(&block)
|
37
|
+
@disconnect_callback = block
|
38
|
+
end
|
42
39
|
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
# Provide a block to be called when this agent's presence changes
|
41
|
+
# The block will be passed the queue, the agent call, and the new presence
|
42
|
+
def self.on_presence_change(&block)
|
43
|
+
@presence_change_callback = block
|
44
|
+
end
|
45
|
+
|
46
|
+
# Provide a block to be called when the agent connection to the callee fails
|
47
|
+
# The block will be passed the queue, the agent call and the client call
|
48
|
+
def self.on_connection_failed(&block)
|
49
|
+
@connection_failed_callback = block
|
50
|
+
end
|
51
|
+
|
52
|
+
def on_call?
|
53
|
+
@presence == :on_call
|
54
|
+
end
|
55
|
+
|
56
|
+
# Called to provide options for calling this agent that are passed to #dial
|
57
|
+
def dial_options_for(queue, queued_call)
|
58
|
+
{}
|
59
|
+
end
|
60
|
+
|
61
|
+
def join(queued_call)
|
62
|
+
# For use in queues that need bridge connections
|
63
|
+
@call.join queued_call
|
64
|
+
end
|
65
|
+
|
66
|
+
# FIXME: Use delegator?
|
67
|
+
def from
|
68
|
+
@call.from
|
69
|
+
end
|
46
70
|
end
|
47
71
|
end
|
48
|
-
|
@@ -6,6 +6,7 @@ require 'electric_slide/agent_strategy/longest_idle'
|
|
6
6
|
class ElectricSlide
|
7
7
|
class CallQueue
|
8
8
|
include Celluloid
|
9
|
+
|
9
10
|
ENDED_CALL_EXCEPTIONS = [
|
10
11
|
Adhearsion::Call::Hangup,
|
11
12
|
Adhearsion::Call::ExpiredError,
|
@@ -24,23 +25,90 @@ class ElectricSlide
|
|
24
25
|
:manual,
|
25
26
|
].freeze
|
26
27
|
|
27
|
-
|
28
|
-
|
28
|
+
Error = Class.new(StandardError)
|
29
|
+
|
30
|
+
MissingAgentError = Class.new(Error)
|
31
|
+
DuplicateAgentError = Class.new(Error)
|
32
|
+
|
33
|
+
class InvalidConnectionType < Error
|
34
|
+
def message
|
35
|
+
"Invalid connection type; must be one of #{CONNECTION_TYPES.join ','}"
|
36
|
+
end
|
29
37
|
end
|
30
38
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
39
|
+
class InvalidRequeueMethod < Error
|
40
|
+
def message
|
41
|
+
"Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :agent_strategy, :connection_type, :agent_return_method
|
46
|
+
|
47
|
+
def self.valid_with?(attrs = {})
|
48
|
+
return false unless Hash === attrs
|
35
49
|
|
36
|
-
|
37
|
-
|
50
|
+
if agent_strategy = attrs[:agent_strategy]
|
51
|
+
begin
|
52
|
+
agent_strategy.new
|
53
|
+
rescue Exception
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
if connection_type = attrs[:connection_type]
|
58
|
+
return false unless valid_connection_type? connection_type
|
59
|
+
end
|
60
|
+
if agent_return_method = attrs[:agent_return_method]
|
61
|
+
return false unless valid_agent_return_method? agent_return_method
|
62
|
+
end
|
38
63
|
|
39
|
-
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.valid_connection_type?(connection_type)
|
68
|
+
CONNECTION_TYPES.include? connection_type
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.valid_agent_return_method?(agent_return_method)
|
72
|
+
AGENT_RETURN_METHODS.include? agent_return_method
|
73
|
+
end
|
74
|
+
|
75
|
+
def initialize(opts = {})
|
40
76
|
@agents = [] # Needed to keep track of global list of agents
|
41
77
|
@queue = [] # Calls waiting for an agent
|
42
78
|
|
43
|
-
|
79
|
+
update(
|
80
|
+
agent_strategy: opts[:agent_strategy] || AgentStrategy::LongestIdle,
|
81
|
+
connection_type: opts[:connection_type] || :call,
|
82
|
+
agent_return_method: opts[:agent_return_method] || :auto
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def update(attrs)
|
87
|
+
attrs.each do |attr, value|
|
88
|
+
setter = "#{attr}="
|
89
|
+
send setter, value if respond_to?(setter)
|
90
|
+
end unless attrs.nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
def agent_strategy=(new_agent_strategy)
|
94
|
+
@agent_strategy = new_agent_strategy
|
95
|
+
|
96
|
+
@strategy = @agent_strategy.new
|
97
|
+
@agents.each do |agent|
|
98
|
+
return_agent agent, agent.presence
|
99
|
+
end
|
100
|
+
|
101
|
+
@agent_strategy
|
102
|
+
end
|
103
|
+
|
104
|
+
def connection_type=(new_connection_type)
|
105
|
+
abort InvalidConnectionType.new unless CallQueue.valid_connection_type? new_connection_type
|
106
|
+
@connection_type = new_connection_type
|
107
|
+
end
|
108
|
+
|
109
|
+
def agent_return_method=(new_agent_return_method)
|
110
|
+
abort InvalidRequeueMethod.new unless CallQueue.valid_agent_return_method? new_agent_return_method
|
111
|
+
@agent_return_method = new_agent_return_method
|
44
112
|
end
|
45
113
|
|
46
114
|
# Checks whether an agent is available to take a call
|
@@ -58,11 +126,13 @@ class ElectricSlide
|
|
58
126
|
@strategy.available_agent_summary
|
59
127
|
end
|
60
128
|
|
61
|
-
# Assigns the first available agent, marking the agent :
|
129
|
+
# Assigns the first available agent, marking the agent :on_call
|
62
130
|
# @return {Agent}
|
63
131
|
def checkout_agent
|
64
132
|
agent = @strategy.checkout_agent
|
65
|
-
agent
|
133
|
+
if agent
|
134
|
+
agent.presence = :on_call
|
135
|
+
end
|
66
136
|
agent
|
67
137
|
end
|
68
138
|
|
@@ -87,46 +157,68 @@ class ElectricSlide
|
|
87
157
|
|
88
158
|
# Registers an agent to the queue
|
89
159
|
# @param [Agent] agent The agent to be added to the queue
|
160
|
+
# @raise ArgumentError if the agent is malformed
|
161
|
+
# @raise DuplicateAgentError if this agent ID already exists
|
162
|
+
# @see #update_agent
|
90
163
|
def add_agent(agent)
|
91
164
|
abort ArgumentError.new("#add_agent called with nil object") if agent.nil?
|
165
|
+
abort DuplicateAgentError.new("Agent is already in the queue") if get_agent(agent.id)
|
166
|
+
|
167
|
+
agent.queue = self
|
168
|
+
|
92
169
|
case @connection_type
|
93
170
|
when :call
|
94
171
|
abort ArgumentError.new("Agent has no callable address") unless agent.address
|
95
172
|
when :bridge
|
96
|
-
|
97
|
-
unless agent.call[:electric_slide_callback_set]
|
98
|
-
agent.call.on_end { remove_agent agent }
|
99
|
-
agent.call[:electric_slide_callback_set] = true
|
100
|
-
end
|
173
|
+
bridged_agent_health_check agent
|
101
174
|
end
|
102
175
|
|
103
176
|
logger.info "Adding agent #{agent} to the queue"
|
104
|
-
@agents << agent
|
177
|
+
@agents << agent
|
105
178
|
@strategy << agent if agent.presence == :available
|
179
|
+
# Fake the presence callback since this is a new agent
|
180
|
+
agent.callback :presence_change, self, agent.call, agent.presence, :unavailable
|
181
|
+
|
106
182
|
check_for_connections
|
107
183
|
end
|
108
184
|
|
109
185
|
# Marks an agent as available to take a call. To be called after an agent completes a call
|
110
186
|
# and is ready to take the next call.
|
111
187
|
# @param [Agent] agent The {Agent} that is being returned to the queue
|
112
|
-
# @param [Symbol]
|
188
|
+
# @param [Symbol] new_presence The {Agent}'s new presence
|
113
189
|
# @param [String, Optional] address The {Agent}'s address. Only specified if it has changed
|
114
|
-
def return_agent(agent,
|
190
|
+
def return_agent(agent, new_presence = :available, address = nil)
|
115
191
|
logger.debug "Returning #{agent} to the queue"
|
116
|
-
|
192
|
+
|
193
|
+
return false unless get_agent(agent.id)
|
194
|
+
|
195
|
+
agent.presence = new_presence
|
117
196
|
agent.address = address if address
|
118
197
|
|
119
|
-
|
198
|
+
case agent.presence
|
199
|
+
when :available
|
200
|
+
bridged_agent_health_check agent
|
201
|
+
|
120
202
|
@strategy << agent
|
121
203
|
check_for_connections
|
204
|
+
when :unavailable
|
205
|
+
@strategy.delete agent
|
122
206
|
end
|
123
207
|
agent
|
124
208
|
end
|
125
209
|
|
210
|
+
# Marks an agent as available to take a call.
|
211
|
+
# @see #return_agent
|
212
|
+
# @raises [ElectricSlide::CallQueue::MissingAgentError] when the agent cannot be returned because they have been explicitly removed.
|
213
|
+
def return_agent!(*args)
|
214
|
+
return_agent(*args) || abort(MissingAgentError.new('Agent is not in the queue. Unable to return agent.'))
|
215
|
+
end
|
216
|
+
|
126
217
|
# Removes an agent from the queue entirely
|
127
218
|
# @param [Agent] agent The {Agent} to be removed from the queue
|
128
219
|
# @return [Agent, Nil] The Agent object if removed, Nil otherwise
|
129
220
|
def remove_agent(agent)
|
221
|
+
agent.presence = :unavailable
|
130
222
|
@strategy.delete agent
|
131
223
|
@agents.delete agent
|
132
224
|
logger.info "Removing agent #{agent} from the queue"
|
@@ -143,8 +235,13 @@ class ElectricSlide
|
|
143
235
|
# to an agent, but the connection fails because the agent is not available.
|
144
236
|
# @param [Adhearsion::Call] call Caller to be added to the queue
|
145
237
|
def priority_enqueue(call)
|
146
|
-
#
|
147
|
-
call
|
238
|
+
# In case this is a re-insert on agent failure...
|
239
|
+
# ... reset `:agent` call variable
|
240
|
+
call[:agent] = nil
|
241
|
+
# ... set, but don't reset, the enqueue time
|
242
|
+
call[:electric_slide_enqueued_at] ||= DateTime.now
|
243
|
+
|
244
|
+
call.on_end { remove_call call }
|
148
245
|
@queue.unshift call
|
149
246
|
|
150
247
|
check_for_connections
|
@@ -155,7 +252,8 @@ class ElectricSlide
|
|
155
252
|
def enqueue(call)
|
156
253
|
ignoring_ended_calls do
|
157
254
|
logger.info "Adding call from #{remote_party call} to the queue"
|
158
|
-
call[:
|
255
|
+
call[:electric_slide_enqueued_at] = DateTime.now
|
256
|
+
call.on_end { remove_call call }
|
159
257
|
@queue << call unless @queue.include? call
|
160
258
|
|
161
259
|
check_for_connections
|
@@ -164,8 +262,12 @@ class ElectricSlide
|
|
164
262
|
|
165
263
|
# Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed.
|
166
264
|
# @param [Adhearsion::Call] call Caller to be removed from the queue
|
167
|
-
def
|
168
|
-
ignoring_ended_calls
|
265
|
+
def remove_call(call)
|
266
|
+
ignoring_ended_calls do
|
267
|
+
unless call[:electric_slide_connected_at]
|
268
|
+
logger.info "Caller #{remote_party call} has abandoned the queue"
|
269
|
+
end
|
270
|
+
end
|
169
271
|
@queue.delete call
|
170
272
|
end
|
171
273
|
|
@@ -178,12 +280,14 @@ class ElectricSlide
|
|
178
280
|
return_agent agent
|
179
281
|
end
|
180
282
|
|
283
|
+
queued_call[:agent] = agent
|
284
|
+
|
181
285
|
logger.info "Connecting #{agent} with #{remote_party queued_call}"
|
182
286
|
case @connection_type
|
183
287
|
when :call
|
184
288
|
call_agent agent, queued_call
|
185
289
|
when :bridge
|
186
|
-
unless agent.call.active?
|
290
|
+
unless agent.call && agent.call.active?
|
187
291
|
logger.warn "Inactive agent call found in #connect, returning caller to queue"
|
188
292
|
priority_enqueue queued_call
|
189
293
|
end
|
@@ -199,6 +303,8 @@ class ElectricSlide
|
|
199
303
|
ignoring_ended_calls do
|
200
304
|
if agent.call && agent.call.active?
|
201
305
|
logger.warn "Dead call exception in #connect but agent call still alive, reinserting into queue"
|
306
|
+
agent.callback :connection_failed, self, agent.call, queued_call
|
307
|
+
|
202
308
|
return_agent agent
|
203
309
|
end
|
204
310
|
end
|
@@ -207,11 +313,12 @@ class ElectricSlide
|
|
207
313
|
def conditionally_return_agent(agent, return_method = @agent_return_method)
|
208
314
|
raise ArgumentError, "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}" unless AGENT_RETURN_METHODS.include? return_method
|
209
315
|
|
210
|
-
if agent && @agents.include?(agent) && agent.
|
316
|
+
if agent && @agents.include?(agent) && agent.on_call? && return_method == :auto
|
211
317
|
logger.info "Returning agent #{agent.id} to queue"
|
212
318
|
return_agent agent
|
213
319
|
else
|
214
320
|
logger.debug "Not returning agent #{agent.inspect} to the queue"
|
321
|
+
return_agent agent, :after_call
|
215
322
|
end
|
216
323
|
end
|
217
324
|
|
@@ -234,6 +341,7 @@ class ElectricSlide
|
|
234
341
|
end
|
235
342
|
|
236
343
|
private
|
344
|
+
|
237
345
|
# Get the caller ID of the remote party.
|
238
346
|
# If this is an OutboundCall, use Call#to
|
239
347
|
# Otherwise, use Call#from
|
@@ -241,7 +349,6 @@ class ElectricSlide
|
|
241
349
|
call.is_a?(Adhearsion::OutboundCall) ? call.to : call.from
|
242
350
|
end
|
243
351
|
|
244
|
-
|
245
352
|
# @private
|
246
353
|
def ignoring_ended_calls
|
247
354
|
yield
|
@@ -254,6 +361,8 @@ class ElectricSlide
|
|
254
361
|
agent_call[:agent] = agent
|
255
362
|
agent_call[:queued_call] = queued_call
|
256
363
|
|
364
|
+
agent.call = agent_call
|
365
|
+
|
257
366
|
# Stash the caller ID so we don't have to try to get it from a dead call object later
|
258
367
|
queued_caller_id = remote_party queued_call
|
259
368
|
|
@@ -273,17 +382,24 @@ class ElectricSlide
|
|
273
382
|
|
274
383
|
# Track whether the agent actually talks to the queued_call
|
275
384
|
connected = false
|
276
|
-
queued_call.
|
385
|
+
queued_call.register_tmp_handler :event, Punchblock::Event::Joined do |event|
|
386
|
+
connected = true
|
387
|
+
queued_call[:electric_slide_connected_at] = event.timestamp
|
388
|
+
end
|
277
389
|
|
278
390
|
agent_call.on_end do |end_event|
|
279
391
|
# Ensure we don't return an agent that was removed or paused
|
280
392
|
conditionally_return_agent agent
|
281
393
|
|
394
|
+
agent.call = nil
|
395
|
+
|
282
396
|
agent.callback :disconnect, self, agent_call, queued_call
|
283
397
|
|
284
398
|
unless connected
|
285
|
-
if queued_call.
|
399
|
+
if queued_call.active?
|
286
400
|
ignoring_ended_calls { priority_enqueue queued_call }
|
401
|
+
agent.callback :connection_failed, self, agent_call, queued_call
|
402
|
+
|
287
403
|
logger.warn "Call did not connect to agent! Agent #{agent.id} call ended with #{end_event.reason}; reinserting caller #{queued_caller_id} into queue"
|
288
404
|
else
|
289
405
|
logger.warn "Caller #{queued_caller_id} hung up before being connected to an agent."
|
@@ -306,8 +422,12 @@ class ElectricSlide
|
|
306
422
|
agent.call.register_tmp_handler :event, Punchblock::Event::Unjoined do
|
307
423
|
agent.callback :disconnect, self, agent.call, queued_call
|
308
424
|
ignoring_ended_calls { queued_call.hangup }
|
309
|
-
ignoring_ended_calls { conditionally_return_agent agent if agent.call.active? }
|
310
|
-
agent.call[:queued_call] = nil
|
425
|
+
ignoring_ended_calls { conditionally_return_agent agent if agent.call && agent.call.active? }
|
426
|
+
agent.call[:queued_call] = nil if agent.call
|
427
|
+
end
|
428
|
+
|
429
|
+
queued_call.register_tmp_handler :event, Punchblock::Event::Joined do |event|
|
430
|
+
queued_call[:electric_slide_connected_at] = event.timestamp
|
311
431
|
end
|
312
432
|
|
313
433
|
agent.callback :connect, self, agent.call, queued_call
|
@@ -315,12 +435,11 @@ class ElectricSlide
|
|
315
435
|
agent.join queued_call if queued_call.active?
|
316
436
|
rescue *ENDED_CALL_EXCEPTIONS
|
317
437
|
ignoring_ended_calls do
|
318
|
-
if agent.call.active?
|
438
|
+
if agent.call && agent.call.active?
|
439
|
+
agent.callback :connection_failed, self, agent.call, queued_call
|
440
|
+
|
319
441
|
logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup"
|
320
442
|
conditionally_return_agent agent, :auto
|
321
|
-
else
|
322
|
-
# Agent's call has ended, so remove him from the queue
|
323
|
-
remove_agent agent
|
324
443
|
end
|
325
444
|
end
|
326
445
|
|
@@ -331,5 +450,20 @@ class ElectricSlide
|
|
331
450
|
end
|
332
451
|
end
|
333
452
|
end
|
453
|
+
|
454
|
+
# @private
|
455
|
+
def bridged_agent_health_check(agent)
|
456
|
+
if agent.presence == :available && @connection_type == :bridge
|
457
|
+
abort ArgumentError.new("Agent has no active call") unless agent.call && agent.call.active?
|
458
|
+
unless agent.call[:electric_slide_callback_set]
|
459
|
+
agent.call[:electric_slide_callback_set] = true
|
460
|
+
queue = self
|
461
|
+
agent.call.on_end do
|
462
|
+
agent.call = nil
|
463
|
+
queue.return_agent agent, :unavailable
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
334
468
|
end
|
335
469
|
end
|