switest 0.5.1 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +29 -25
- data/lib/switest/agent.rb +34 -9
- data/lib/switest/assertions.rb +51 -0
- data/lib/switest/call.rb +196 -0
- data/lib/switest/client.rb +137 -0
- data/lib/switest/{esl/escaper.rb → escaper.rb} +1 -3
- data/lib/switest/events.rb +16 -65
- data/lib/switest/{esl/from_parser.rb → from_parser.rb} +1 -3
- data/lib/switest/scenario.rb +11 -52
- data/lib/switest/session.rb +49 -0
- data/lib/switest/version.rb +1 -1
- data/lib/switest.rb +6 -6
- metadata +10 -11
- data/lib/switest/case_insensitive_hash.rb +0 -17
- data/lib/switest/esl/call.rb +0 -209
- data/lib/switest/esl/client.rb +0 -213
- data/lib/switest/esl/connection.rb +0 -226
- data/lib/switest/esl/event.rb +0 -69
data/lib/switest/events.rb
CHANGED
|
@@ -1,85 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Switest
|
|
4
|
-
#
|
|
5
|
-
# Supports hash equality, regex, array membership, and
|
|
4
|
+
# Lightweight one-shot event emitter with guard support for conditional
|
|
5
|
+
# event handling. Supports hash equality, regex, array membership, and
|
|
6
|
+
# proc guards.
|
|
7
|
+
#
|
|
8
|
+
# Used for routing inbound call offers from Client to Agent.listen_for_call.
|
|
6
9
|
class Events
|
|
7
10
|
def initialize
|
|
8
11
|
@handlers = Hash.new { |h, k| h[k] = [] }
|
|
9
|
-
@handler_id = 0
|
|
10
|
-
@mutex = Mutex.new
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Register a permanent event handler with optional guards
|
|
14
|
-
# @param event [Symbol] Event name
|
|
15
|
-
# @param guards [Hash] Guard conditions (hash equality, regex, array, or proc)
|
|
16
|
-
# @return [Integer] Handler ID for later removal
|
|
17
|
-
def on(event, guards = {}, &block)
|
|
18
|
-
@mutex.synchronize do
|
|
19
|
-
@handler_id += 1
|
|
20
|
-
@handlers[event] << {
|
|
21
|
-
id: @handler_id,
|
|
22
|
-
guards: guards,
|
|
23
|
-
callback: block,
|
|
24
|
-
once: false
|
|
25
|
-
}
|
|
26
|
-
@handler_id
|
|
27
|
-
end
|
|
28
12
|
end
|
|
29
13
|
|
|
30
14
|
# Register a one-time event handler with optional guards
|
|
31
|
-
# @param event [Symbol] Event name
|
|
32
|
-
# @param guards [Hash] Guard conditions
|
|
33
|
-
# @return [Integer] Handler ID
|
|
34
15
|
def once(event, guards = {}, &block)
|
|
35
|
-
@
|
|
36
|
-
@handler_id += 1
|
|
37
|
-
@handlers[event] << {
|
|
38
|
-
id: @handler_id,
|
|
39
|
-
guards: guards,
|
|
40
|
-
callback: block,
|
|
41
|
-
once: true
|
|
42
|
-
}
|
|
43
|
-
@handler_id
|
|
44
|
-
end
|
|
16
|
+
@handlers[event] << { guards: guards, callback: block }
|
|
45
17
|
end
|
|
46
18
|
|
|
47
|
-
# Emit an event, triggering all matching handlers
|
|
48
|
-
# @param event [Symbol] Event name
|
|
49
|
-
# @param data [Hash] Event data to pass to handlers
|
|
19
|
+
# Emit an event, triggering and removing all matching handlers
|
|
50
20
|
def emit(event, data = {})
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@mutex.synchronize do
|
|
55
|
-
@handlers[event].each do |handler|
|
|
56
|
-
if guards_match?(handler[:guards], data)
|
|
57
|
-
handlers_to_call << handler
|
|
58
|
-
handlers_to_remove << handler[:id] if handler[:once]
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
21
|
+
matched = []
|
|
22
|
+
remaining = []
|
|
62
23
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
handlers_to_remove.each do |id|
|
|
67
|
-
@handlers[event].reject! { |h| h[:id] == id }
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Remove handler(s) for an event
|
|
73
|
-
# @param event [Symbol] Event name
|
|
74
|
-
# @param handler_id [Integer, nil] Specific handler ID, or nil to remove all
|
|
75
|
-
def off(event, handler_id = nil)
|
|
76
|
-
@mutex.synchronize do
|
|
77
|
-
if handler_id
|
|
78
|
-
@handlers[event].reject! { |h| h[:id] == handler_id }
|
|
24
|
+
@handlers[event].each do |handler|
|
|
25
|
+
if guards_match?(handler[:guards], data)
|
|
26
|
+
matched << handler
|
|
79
27
|
else
|
|
80
|
-
|
|
28
|
+
remaining << handler
|
|
81
29
|
end
|
|
82
30
|
end
|
|
31
|
+
|
|
32
|
+
@handlers[event] = remaining
|
|
33
|
+
matched.each { |handler| handler[:callback].call(data) }
|
|
83
34
|
end
|
|
84
35
|
|
|
85
36
|
private
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Switest
|
|
4
|
-
|
|
5
|
-
# Parses a `from` string into FreeSWITCH channel variables,
|
|
4
|
+
# Parses a `from` string into FreeSWITCH channel variables,
|
|
6
5
|
# replicating mod_rayo's parse_dial_from() behavior.
|
|
7
6
|
#
|
|
8
7
|
# Algorithm (matching mod_rayo):
|
|
@@ -100,6 +99,5 @@ module Switest
|
|
|
100
99
|
|
|
101
100
|
vars
|
|
102
101
|
end
|
|
103
|
-
end
|
|
104
102
|
end
|
|
105
103
|
end
|
data/lib/switest/scenario.rb
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "minitest/test"
|
|
4
|
-
require "
|
|
4
|
+
require "async"
|
|
5
5
|
|
|
6
6
|
module Switest
|
|
7
7
|
class Scenario < Minitest::Test
|
|
8
|
+
include Assertions
|
|
9
|
+
|
|
8
10
|
# Make Agent accessible to subclasses
|
|
9
11
|
Agent = Switest::Agent
|
|
10
12
|
|
|
13
|
+
# Run each test inside an async reactor so that fibers, conditions,
|
|
14
|
+
# and async I/O work transparently for users extending Scenario.
|
|
15
|
+
def run(...)
|
|
16
|
+
Sync { super }
|
|
17
|
+
end
|
|
18
|
+
|
|
11
19
|
def setup
|
|
12
20
|
@events = Events.new
|
|
13
|
-
@client =
|
|
21
|
+
@client = Client.new
|
|
14
22
|
@client.start
|
|
15
23
|
|
|
16
24
|
# Route inbound calls through events system
|
|
@@ -20,7 +28,7 @@ module Switest
|
|
|
20
28
|
from: call.from,
|
|
21
29
|
call: call,
|
|
22
30
|
headers: call.headers,
|
|
23
|
-
profile: call.headers[
|
|
31
|
+
profile: call.headers[:variable_sofia_profile_name]
|
|
24
32
|
})
|
|
25
33
|
end
|
|
26
34
|
|
|
@@ -37,54 +45,5 @@ module Switest
|
|
|
37
45
|
def hangup_all(cause: "NORMAL_CLEARING", timeout: 5)
|
|
38
46
|
@client&.hangup_all(cause: cause, timeout: timeout)
|
|
39
47
|
end
|
|
40
|
-
|
|
41
|
-
# Assertions
|
|
42
|
-
def assert_call(agent, timeout: 5)
|
|
43
|
-
success = agent.wait_for_call(timeout: timeout)
|
|
44
|
-
assert success, "Expected agent to receive a call within #{timeout} seconds"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def assert_no_call(agent, timeout: 2)
|
|
48
|
-
sleep timeout
|
|
49
|
-
refute agent.call?, "Expected agent to not have received a call"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def assert_answered(agent, timeout: 5)
|
|
53
|
-
assert agent.call?, "Agent has no call"
|
|
54
|
-
success = agent.wait_for_answer(timeout: timeout)
|
|
55
|
-
assert success, "Expected call to be answered within #{timeout} seconds"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def assert_bridged(agent, timeout: 5)
|
|
59
|
-
assert agent.call?, "Agent has no call"
|
|
60
|
-
success = agent.wait_for_bridge(timeout: timeout)
|
|
61
|
-
assert success, "Expected call to be bridged within #{timeout} seconds"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def assert_hungup(agent, timeout: 5)
|
|
65
|
-
assert agent.call?, "Agent has no call"
|
|
66
|
-
success = agent.wait_for_end(timeout: timeout)
|
|
67
|
-
assert success, "Expected call to be hung up within #{timeout} seconds"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def assert_not_hungup(agent, timeout: 2)
|
|
71
|
-
assert agent.call?, "Agent has no call"
|
|
72
|
-
sleep timeout
|
|
73
|
-
refute agent.ended?, "Expected call to still be active"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def assert_dtmf(agent, expected_dtmf, timeout: 5, after: 1, &block)
|
|
77
|
-
assert agent.call?, "Agent has no call"
|
|
78
|
-
|
|
79
|
-
if block
|
|
80
|
-
agent.flush_dtmf
|
|
81
|
-
Concurrent::ScheduledTask.execute(after) { block.call }
|
|
82
|
-
received = agent.receive_dtmf(count: expected_dtmf.length, timeout: timeout + after)
|
|
83
|
-
else
|
|
84
|
-
received = agent.receive_dtmf(count: expected_dtmf.length, timeout: timeout)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
assert_equal expected_dtmf, received, "Expected DTMF '#{expected_dtmf}' but received '#{received}'"
|
|
88
|
-
end
|
|
89
48
|
end
|
|
90
49
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "librevox"
|
|
4
|
+
require "async/promise"
|
|
5
|
+
|
|
6
|
+
module Switest
|
|
7
|
+
# Inbound ESL session built on librevox.
|
|
8
|
+
#
|
|
9
|
+
# A single connection to FreeSWITCH that handles both commands and events
|
|
10
|
+
# (filtered by UUID). Events are dispatched to Call objects via the
|
|
11
|
+
# call_registry. Unknown CHANNEL_PARK events trigger the offer_handler for
|
|
12
|
+
# inbound call detection.
|
|
13
|
+
#
|
|
14
|
+
# Class-level state (call_registry, offer_handler, connection_promise) is used
|
|
15
|
+
# because librevox instantiates Session internally — we cannot inject
|
|
16
|
+
# dependencies via the constructor. Client sets these before starting the
|
|
17
|
+
# connection and clears them on stop.
|
|
18
|
+
class Session < Librevox::Listener::Inbound
|
|
19
|
+
class << self
|
|
20
|
+
attr_accessor :call_registry # { uuid => Call }
|
|
21
|
+
attr_accessor :offer_handler # Proc for inbound call offers
|
|
22
|
+
attr_accessor :connection_promise # Async::Promise resolved when connected
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def connection_completed
|
|
26
|
+
self.class.connection_promise&.resolve(self)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_event(event)
|
|
30
|
+
uuid = event.content[:unique_id]
|
|
31
|
+
call = self.class.call_registry&.[](uuid)
|
|
32
|
+
|
|
33
|
+
case event.event
|
|
34
|
+
when "CHANNEL_CALLSTATE", "CHANNEL_HANGUP_COMPLETE", "DTMF"
|
|
35
|
+
call&.handle_event(event)
|
|
36
|
+
when "CHANNEL_PARK"
|
|
37
|
+
if call
|
|
38
|
+
call.handle_event(event)
|
|
39
|
+
else
|
|
40
|
+
self.class.offer_handler&.call(event)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def bgapi(cmd)
|
|
46
|
+
send_message("bgapi #{cmd}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/switest/version.rb
CHANGED
data/lib/switest.rb
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "switest/version"
|
|
4
4
|
require_relative "switest/configuration"
|
|
5
|
-
require_relative "switest/case_insensitive_hash"
|
|
6
5
|
require_relative "switest/events"
|
|
7
|
-
require_relative "switest/
|
|
8
|
-
require_relative "switest/
|
|
9
|
-
require_relative "switest/
|
|
10
|
-
require_relative "switest/
|
|
11
|
-
require_relative "switest/
|
|
6
|
+
require_relative "switest/escaper"
|
|
7
|
+
require_relative "switest/from_parser"
|
|
8
|
+
require_relative "switest/session"
|
|
9
|
+
require_relative "switest/call"
|
|
10
|
+
require_relative "switest/client"
|
|
12
11
|
require_relative "switest/agent"
|
|
12
|
+
require_relative "switest/assertions"
|
|
13
13
|
require_relative "switest/scenario"
|
|
14
14
|
|
|
15
15
|
module Switest
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: switest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Relatel A/S
|
|
@@ -11,19 +11,19 @@ cert_chain: []
|
|
|
11
11
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: librevox
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '1.
|
|
19
|
+
version: '1.0'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '1.
|
|
26
|
+
version: '1.0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: minitest
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -56,16 +56,15 @@ files:
|
|
|
56
56
|
- README.md
|
|
57
57
|
- lib/switest.rb
|
|
58
58
|
- lib/switest/agent.rb
|
|
59
|
-
- lib/switest/
|
|
59
|
+
- lib/switest/assertions.rb
|
|
60
|
+
- lib/switest/call.rb
|
|
61
|
+
- lib/switest/client.rb
|
|
60
62
|
- lib/switest/configuration.rb
|
|
61
|
-
- lib/switest/
|
|
62
|
-
- lib/switest/esl/client.rb
|
|
63
|
-
- lib/switest/esl/connection.rb
|
|
64
|
-
- lib/switest/esl/escaper.rb
|
|
65
|
-
- lib/switest/esl/event.rb
|
|
66
|
-
- lib/switest/esl/from_parser.rb
|
|
63
|
+
- lib/switest/escaper.rb
|
|
67
64
|
- lib/switest/events.rb
|
|
65
|
+
- lib/switest/from_parser.rb
|
|
68
66
|
- lib/switest/scenario.rb
|
|
67
|
+
- lib/switest/session.rb
|
|
69
68
|
- lib/switest/version.rb
|
|
70
69
|
homepage: https://github.com/relatel/switest
|
|
71
70
|
licenses:
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Switest
|
|
4
|
-
# Hash subclass with case-insensitive key lookup
|
|
5
|
-
class CaseInsensitiveHash < Hash
|
|
6
|
-
def self.from(hash)
|
|
7
|
-
new.merge!(hash)
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def [](key)
|
|
11
|
-
return super if key?(key)
|
|
12
|
-
key_downcase = key.downcase
|
|
13
|
-
found_key = keys.find { |k| k.downcase == key_downcase }
|
|
14
|
-
found_key ? super(found_key) : nil
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
data/lib/switest/esl/call.rb
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "concurrent"
|
|
4
|
-
|
|
5
|
-
module Switest
|
|
6
|
-
module ESL
|
|
7
|
-
class Call
|
|
8
|
-
attr_reader :id, :to, :from, :headers, :direction
|
|
9
|
-
attr_reader :start_time, :answer_time, :end_time, :end_reason
|
|
10
|
-
attr_reader :state
|
|
11
|
-
|
|
12
|
-
def initialize(id:, connection:, direction:, to: nil, from: nil, headers: {})
|
|
13
|
-
@id = id
|
|
14
|
-
@connection = connection
|
|
15
|
-
@direction = direction # :inbound or :outbound
|
|
16
|
-
@to = to
|
|
17
|
-
@from = from
|
|
18
|
-
@headers = Switest::CaseInsensitiveHash.from(headers)
|
|
19
|
-
|
|
20
|
-
@state = :offered
|
|
21
|
-
@start_time = Time.now
|
|
22
|
-
@answer_time = nil
|
|
23
|
-
@end_time = nil
|
|
24
|
-
@end_reason = nil
|
|
25
|
-
|
|
26
|
-
@bridged = false
|
|
27
|
-
@dtmf_buffer = Queue.new
|
|
28
|
-
@callbacks = { answer: [], bridge: [], end: [] }
|
|
29
|
-
@mutex = Mutex.new
|
|
30
|
-
|
|
31
|
-
@answered_latch = Concurrent::CountDownLatch.new(1)
|
|
32
|
-
@bridged_latch = Concurrent::CountDownLatch.new(1)
|
|
33
|
-
@ended_latch = Concurrent::CountDownLatch.new(1)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# State queries
|
|
37
|
-
def alive?
|
|
38
|
-
@state != :ended
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def active?
|
|
42
|
-
@state == :answered
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def answered?
|
|
46
|
-
@state == :answered || (@state == :ended && @answer_time)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def ended?
|
|
50
|
-
@state == :ended
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def bridged?
|
|
54
|
-
@bridged
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def inbound?
|
|
58
|
-
@direction == :inbound
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def outbound?
|
|
62
|
-
@direction == :outbound
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Actions
|
|
66
|
-
def answer(wait: 5)
|
|
67
|
-
return unless @state == :offered && inbound?
|
|
68
|
-
sendmsg("execute", "answer")
|
|
69
|
-
return unless wait
|
|
70
|
-
wait_for_answer(timeout: wait)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def hangup(cause = "NORMAL_CLEARING", wait: 5)
|
|
74
|
-
return if ended?
|
|
75
|
-
msg = +"sendmsg #{@id}\n"
|
|
76
|
-
msg << "call-command: hangup\n"
|
|
77
|
-
msg << "hangup-cause: #{cause}"
|
|
78
|
-
@connection.send_command(msg)
|
|
79
|
-
return unless wait
|
|
80
|
-
wait_for_end(timeout: wait)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def reject(reason = :decline, wait: 5)
|
|
84
|
-
return unless @state == :offered && inbound?
|
|
85
|
-
cause = case reason
|
|
86
|
-
when :busy then "USER_BUSY"
|
|
87
|
-
when :decline then "CALL_REJECTED"
|
|
88
|
-
else "CALL_REJECTED"
|
|
89
|
-
end
|
|
90
|
-
hangup(cause, wait: wait)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def play_audio(url, wait: true)
|
|
94
|
-
sendmsg("execute", "playback", url, event_lock: wait)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def send_dtmf(digits, wait: true)
|
|
98
|
-
play_audio("tone_stream://d=200;w=250;#{digits}", wait: wait)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def flush_dtmf
|
|
102
|
-
@dtmf_buffer.clear
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def receive_dtmf(count: 1, timeout: 5)
|
|
106
|
-
digits = String.new
|
|
107
|
-
deadline = Time.now + timeout
|
|
108
|
-
|
|
109
|
-
count.times do
|
|
110
|
-
remaining = deadline - Time.now
|
|
111
|
-
break if remaining <= 0
|
|
112
|
-
|
|
113
|
-
begin
|
|
114
|
-
digit = @dtmf_buffer.pop(timeout: remaining)
|
|
115
|
-
digits << digit if digit
|
|
116
|
-
rescue ThreadError
|
|
117
|
-
break # Timeout
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
digits
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Callbacks
|
|
125
|
-
def on_answer(&block)
|
|
126
|
-
@mutex.synchronize { @callbacks[:answer] << block }
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def on_bridge(&block)
|
|
130
|
-
@mutex.synchronize { @callbacks[:bridge] << block }
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def on_end(&block)
|
|
134
|
-
@mutex.synchronize { @callbacks[:end] << block }
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Blocking waits
|
|
138
|
-
def wait_for_answer(timeout: 5)
|
|
139
|
-
@answered_latch.wait(timeout)
|
|
140
|
-
answered?
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def wait_for_bridge(timeout: 5)
|
|
144
|
-
@bridged_latch.wait(timeout)
|
|
145
|
-
bridged?
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def wait_for_end(timeout: 5)
|
|
149
|
-
@ended_latch.wait(timeout)
|
|
150
|
-
ended?
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Internal state updates (called by Client)
|
|
154
|
-
def handle_answer
|
|
155
|
-
@mutex.synchronize do
|
|
156
|
-
return if @state == :ended
|
|
157
|
-
@state = :answered
|
|
158
|
-
@answer_time = Time.now
|
|
159
|
-
end
|
|
160
|
-
@answered_latch.count_down
|
|
161
|
-
fire_callbacks(:answer)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def handle_bridge
|
|
165
|
-
@mutex.synchronize do
|
|
166
|
-
return if @state == :ended
|
|
167
|
-
@bridged = true
|
|
168
|
-
end
|
|
169
|
-
@bridged_latch.count_down
|
|
170
|
-
fire_callbacks(:bridge)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def handle_hangup(cause, headers = {})
|
|
174
|
-
@mutex.synchronize do
|
|
175
|
-
return if @state == :ended
|
|
176
|
-
@state = :ended
|
|
177
|
-
@end_time = Time.now
|
|
178
|
-
@end_reason = cause
|
|
179
|
-
@headers.merge!(headers)
|
|
180
|
-
end
|
|
181
|
-
@answered_latch.count_down # Release any waiting threads
|
|
182
|
-
@bridged_latch.count_down
|
|
183
|
-
@ended_latch.count_down
|
|
184
|
-
fire_callbacks(:end)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def handle_dtmf(digit)
|
|
188
|
-
@dtmf_buffer.push(digit)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
private
|
|
192
|
-
|
|
193
|
-
def sendmsg(command, app = nil, arg = nil, event_lock: false)
|
|
194
|
-
msg = +"sendmsg #{@id}\n"
|
|
195
|
-
msg << "call-command: #{command}\n"
|
|
196
|
-
msg << "execute-app-name: #{app}\n" if app
|
|
197
|
-
msg << "execute-app-arg: #{arg}\n" if arg
|
|
198
|
-
msg << "event-lock: true\n" if event_lock
|
|
199
|
-
@connection.send_command(msg.chomp)
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def fire_callbacks(type)
|
|
204
|
-
callbacks = @mutex.synchronize { @callbacks[type].dup }
|
|
205
|
-
callbacks.each { |cb| cb.call(self) }
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|