signalwire 1.4.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +55 -0
- data/.rubocop.yml +14 -2
- data/AUTHORS.md +1 -0
- data/CHANGELOG.md +30 -5
- data/Gemfile +4 -18
- data/LICENSE +21 -0
- data/README.md +22 -82
- data/Rakefile +0 -17
- data/examples/relay/inbound_consumer.rb +26 -0
- data/examples/relay/inbound_dial.rb +28 -0
- data/examples/relay/outbound_collect.rb +27 -0
- data/examples/relay/outbound_consumer.rb +22 -0
- data/examples/relay/outbound_record.rb +25 -0
- data/lib/signalwire.rb +13 -0
- data/lib/signalwire/blade.rb +12 -0
- data/lib/signalwire/blade/connection.rb +200 -0
- data/lib/signalwire/blade/event_handler.rb +15 -0
- data/lib/signalwire/blade/message.rb +38 -0
- data/lib/signalwire/blade/message/connect.rb +18 -0
- data/lib/signalwire/blade/message/execute.rb +16 -0
- data/lib/signalwire/blade/message/subscribe.rb +15 -0
- data/lib/signalwire/common.rb +6 -0
- data/lib/signalwire/logger.rb +27 -0
- data/lib/signalwire/relay.rb +40 -0
- data/lib/signalwire/relay/calling.rb +82 -0
- data/lib/signalwire/relay/calling/action.rb +16 -0
- data/lib/signalwire/relay/calling/action/connect_action.rb +11 -0
- data/lib/signalwire/relay/calling/action/play_action.rb +15 -0
- data/lib/signalwire/relay/calling/action/prompt_action.rb +15 -0
- data/lib/signalwire/relay/calling/action/record_action.rb +15 -0
- data/lib/signalwire/relay/calling/call.rb +210 -0
- data/lib/signalwire/relay/calling/call_convenience_methods.rb +79 -0
- data/lib/signalwire/relay/calling/component.rb +115 -0
- data/lib/signalwire/relay/calling/component/answer.rb +27 -0
- data/lib/signalwire/relay/calling/component/await.rb +20 -0
- data/lib/signalwire/relay/calling/component/connect.rb +45 -0
- data/lib/signalwire/relay/calling/component/dial.rb +34 -0
- data/lib/signalwire/relay/calling/component/hangup.rb +41 -0
- data/lib/signalwire/relay/calling/component/play.rb +46 -0
- data/lib/signalwire/relay/calling/component/prompt.rb +62 -0
- data/lib/signalwire/relay/calling/component/record.rb +53 -0
- data/lib/signalwire/relay/calling/control_component.rb +35 -0
- data/lib/signalwire/relay/calling/result.rb +16 -0
- data/lib/signalwire/relay/calling/result/answer_result.rb +6 -0
- data/lib/signalwire/relay/calling/result/connect_result.rb +9 -0
- data/lib/signalwire/relay/calling/result/dial_result.rb +7 -0
- data/lib/signalwire/relay/calling/result/hangup_result.rb +7 -0
- data/lib/signalwire/relay/calling/result/play_result.rb +6 -0
- data/lib/signalwire/relay/calling/result/prompt_result.rb +11 -0
- data/lib/signalwire/relay/calling/result/record_result.rb +7 -0
- data/lib/signalwire/relay/client.rb +147 -0
- data/lib/signalwire/relay/constants.rb +109 -0
- data/lib/signalwire/relay/consumer.rb +88 -0
- data/lib/signalwire/relay/event.rb +41 -0
- data/lib/signalwire/relay/request.rb +6 -0
- data/lib/signalwire/rest/client.rb +5 -5
- data/lib/signalwire/sdk/fax_response.rb +3 -3
- data/lib/signalwire/sdk/messaging_response.rb +0 -2
- data/lib/signalwire/sdk/twilio_set_fax.rb +9 -8
- data/lib/signalwire/sdk/twilio_set_host.rb +8 -3
- data/lib/signalwire/version.rb +5 -0
- data/signalwire.gemspec +36 -106
- metadata +173 -76
- data/LICENSE.txt +0 -20
- data/VERSION +0 -1
- data/spec/signalwire/rest/client_spec.rb +0 -30
- data/spec/signalwire/rest/integration_spec.rb +0 -102
- data/spec/signalwire/sdk/configuration_spec.rb +0 -28
- data/spec/signalwire/sdk/fax_response_spec.rb +0 -26
- data/spec/signalwire/sdk/messaging_response_spec.rb +0 -18
- data/spec/signalwire/sdk/voice_response_spec.rb +0 -20
- data/spec/signalwire/sdk_spec.rb +0 -27
- data/spec/spec_helper.rb +0 -119
- data/spec/vcr_cassettes/accounts.yml +0 -27
- data/spec/vcr_cassettes/applications.yml +0 -29
- data/spec/vcr_cassettes/get_fax.yml +0 -25
- data/spec/vcr_cassettes/get_fax_media_instance.yml +0 -27
- data/spec/vcr_cassettes/get_fax_media_list.yml +0 -47
- data/spec/vcr_cassettes/list_faxes.yml +0 -25
- data/spec/vcr_cassettes/local_numbers.yml +0 -26
- data/spec/vcr_cassettes/recordings.yml +0 -27
- data/spec/vcr_cassettes/send_fax.yml +0 -27
- data/spec/vcr_cassettes/toll_free_numbers.yml +0 -34
- data/spec/vcr_cassettes/transcriptions.yml +0 -28
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Signalwire::Relay::Calling
|
6
|
+
class Action
|
7
|
+
extend Forwardable
|
8
|
+
attr_reader :component
|
9
|
+
|
10
|
+
def_delegators :@component, :control_id, :payload, :completed, :state
|
11
|
+
|
12
|
+
def initialize(component:)
|
13
|
+
@component = component
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Signalwire::Relay::Calling
|
6
|
+
class Call
|
7
|
+
include Signalwire::Logger
|
8
|
+
include Signalwire::Common
|
9
|
+
include Signalwire::Blade::EventHandler
|
10
|
+
include Signalwire::Relay::Calling::CallConvenienceMethods
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :device, :type, :node_id, :context, :from, :to,
|
14
|
+
:timeout, :tag, :client, :state, :previous_state, :components,
|
15
|
+
:busy, :failed
|
16
|
+
def_delegators :@client, :relay_execute
|
17
|
+
|
18
|
+
def self.from_event(client, event)
|
19
|
+
new(client, event.call_params)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(client, call_options)
|
23
|
+
@client = client
|
24
|
+
|
25
|
+
@id = call_options[:call_id]
|
26
|
+
setup_call_options(call_options)
|
27
|
+
@type = @device[:type]
|
28
|
+
|
29
|
+
@from = @device[:params][:from_number]
|
30
|
+
@to = @device[:params][:to_number]
|
31
|
+
@timeout = @device[:params][:timeout] || 30
|
32
|
+
@tag = SecureRandom.uuid
|
33
|
+
|
34
|
+
@components = []
|
35
|
+
|
36
|
+
setup_call_event_handlers
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup_call_event_handlers
|
40
|
+
@client.on(:event, proc { |evt| call_match_event(evt) }) do |event|
|
41
|
+
case event.event_type
|
42
|
+
when 'calling.call.connect'
|
43
|
+
change_connect_state(event.call_params[:connect_state])
|
44
|
+
when 'calling.call.state'
|
45
|
+
change_call_state(event.event_params)
|
46
|
+
end
|
47
|
+
|
48
|
+
broadcast :event, event
|
49
|
+
broadcast :state_change, event
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def change_call_state(event_params)
|
54
|
+
call_state = event_params[:params]
|
55
|
+
@previous_state = @state
|
56
|
+
@state = call_state[:call_state]
|
57
|
+
broadcast :call_state_change, previous_state: @previous_state, state: @state
|
58
|
+
|
59
|
+
update_call_fields(call_state)
|
60
|
+
broadcast @state.to_sym, previous_state: @previous_state, state: @state
|
61
|
+
finish_call(event_params) if @state == Relay::CallState::ENDED
|
62
|
+
end
|
63
|
+
|
64
|
+
def update_call_fields(call_state)
|
65
|
+
@id = call_state[:call_id] if call_state[:call_id]
|
66
|
+
@node_id = call_state[:node_id] if call_state[:node_id]
|
67
|
+
end
|
68
|
+
|
69
|
+
def change_connect_state(new_connect_state)
|
70
|
+
@previous_connect_state = @connect_state
|
71
|
+
@connect_state = new_connect_state
|
72
|
+
broadcast :connect_state_change, previous_state: @previous_connect_state, state: @connect_state
|
73
|
+
end
|
74
|
+
|
75
|
+
def call_match_event(event)
|
76
|
+
event.event_type.match(/calling\.call/) &&
|
77
|
+
!event.event_type.match(/receive/) &&
|
78
|
+
(event.call_id == id || event.call_params[:tag] == tag)
|
79
|
+
end
|
80
|
+
|
81
|
+
def id
|
82
|
+
@id ||= SecureRandom.uuid
|
83
|
+
end
|
84
|
+
|
85
|
+
def answered?
|
86
|
+
@state == 'answered'
|
87
|
+
end
|
88
|
+
|
89
|
+
def ended?
|
90
|
+
@state == 'ending' || @state == 'ended'
|
91
|
+
end
|
92
|
+
|
93
|
+
def active?
|
94
|
+
!ended?
|
95
|
+
end
|
96
|
+
|
97
|
+
def answer
|
98
|
+
answer_component = Signalwire::Relay::Calling::Answer.new(call: self)
|
99
|
+
answer_component.wait_for(Relay::CallState::ANSWERED, Relay::CallState::ENDING, Relay::CallState::ENDED)
|
100
|
+
AnswerResult.new(component: answer_component)
|
101
|
+
end
|
102
|
+
|
103
|
+
def play(play_object)
|
104
|
+
play_component = Signalwire::Relay::Calling::Play.new(call: self, play: play_object)
|
105
|
+
play_component.wait_for(Relay::CallPlayState::FINISHED, Relay::CallPlayState::ERROR)
|
106
|
+
PlayResult.new(component: play_component)
|
107
|
+
end
|
108
|
+
|
109
|
+
def play!(play_object)
|
110
|
+
play_component = Signalwire::Relay::Calling::Play.new(call: self, play: play_object)
|
111
|
+
play_component.execute
|
112
|
+
PlayAction.new(component: play_component)
|
113
|
+
end
|
114
|
+
|
115
|
+
def prompt(collect_object, play_object)
|
116
|
+
component = Prompt.new(call: self, collect: collect_object, play: play_object)
|
117
|
+
component.wait_for(Relay::CallPromptState::ERROR, Relay::CallPromptState::NO_INPUT,
|
118
|
+
Relay::CallPromptState::NO_MATCH, Relay::CallPromptState::DIGIT,
|
119
|
+
Relay::CallPromptState::SPEECH)
|
120
|
+
PromptResult.new(component: component)
|
121
|
+
end
|
122
|
+
|
123
|
+
def prompt!(collect_object, play_object)
|
124
|
+
component = Prompt,new(call: self, collect: collect_object, play: play_object)
|
125
|
+
component.execute
|
126
|
+
PromptAction.new(component: component)
|
127
|
+
end
|
128
|
+
|
129
|
+
def connect(devices_object)
|
130
|
+
component = Connect.new(call: self, devices: devices_object)
|
131
|
+
component.wait_for(Relay::CallConnectState::CONNECTED, Relay::CallConnectState::FAILED)
|
132
|
+
ConnectResult.new(component: component)
|
133
|
+
end
|
134
|
+
|
135
|
+
def connect!(devices_object)
|
136
|
+
component = Connect.new(call: self, devices: devices_object)
|
137
|
+
component.execute
|
138
|
+
ConnectAction.new(component: component)
|
139
|
+
end
|
140
|
+
|
141
|
+
def record(record_object)
|
142
|
+
component = Record.new(call: self, record: record_object)
|
143
|
+
component.wait_for(Relay::CallRecordState::NO_INPUT, Relay::CallRecordState::FINISHED)
|
144
|
+
RecordResult.new(component: component)
|
145
|
+
end
|
146
|
+
|
147
|
+
def record!(record_object)
|
148
|
+
component = Record.new(call: self, record: record_object)
|
149
|
+
component.execute
|
150
|
+
RecordAction.new(component: component)
|
151
|
+
end
|
152
|
+
|
153
|
+
def hangup(reason = 'hangup')
|
154
|
+
hangup_component = Signalwire::Relay::Calling::Hangup.new(call: self, reason: reason)
|
155
|
+
hangup_component.wait_for(Relay::CallState::ENDED)
|
156
|
+
HangupResult.new(component: hangup_component)
|
157
|
+
end
|
158
|
+
|
159
|
+
def dial
|
160
|
+
dial_component = Signalwire::Relay::Calling::Dial.new(call: self)
|
161
|
+
dial_component.wait_for(Relay::CallState::ANSWERED, Relay::CallState::ENDING, Relay::CallState::ENDED)
|
162
|
+
DialResult.new(component: dial_component)
|
163
|
+
end
|
164
|
+
|
165
|
+
def wait_for(*events)
|
166
|
+
events = [Relay::CallState::ENDED] if events.empty?
|
167
|
+
|
168
|
+
current_state_index = Relay::CALL_STATES.find_index(@state)
|
169
|
+
max_index = events.map { |evt| Relay::CALL_STATES.find_index(evt) }.max
|
170
|
+
|
171
|
+
return true if current_state_index >= max_index
|
172
|
+
|
173
|
+
component = Await.new(call: self)
|
174
|
+
component.wait_for(*events)
|
175
|
+
component.successful
|
176
|
+
end
|
177
|
+
|
178
|
+
def register_component(component)
|
179
|
+
@components << component
|
180
|
+
end
|
181
|
+
|
182
|
+
def terminate_components(params = {})
|
183
|
+
@components.each do |comp|
|
184
|
+
comp.terminate(params) unless component.completed
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def finish_call(params)
|
189
|
+
terminate_components(params)
|
190
|
+
client.calling.end_call(id)
|
191
|
+
@busy = true if params[:reason] == Relay::DisconnectReason::BUSY
|
192
|
+
@failed = true if params[:reason] == Relay::DisconnectReason::FAILED
|
193
|
+
broadcast :ended, previous_state: @previous_state, state: @state
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
def setup_call_options(call_options)
|
199
|
+
@node_id = call_options[:node_id]
|
200
|
+
@context = call_options[:context]
|
201
|
+
@previous_state = nil
|
202
|
+
@state = call_options[:call_state]
|
203
|
+
@previous_connect_state = nil
|
204
|
+
@connect_state = call_options[:connect_state]
|
205
|
+
@device = call_options[:device]
|
206
|
+
@busy = false
|
207
|
+
@failed = false
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Signalwire::Relay::Calling
|
2
|
+
module CallConvenienceMethods
|
3
|
+
def play_audio(url)
|
4
|
+
play audio_payload(url)
|
5
|
+
end
|
6
|
+
|
7
|
+
def play_audio!(url)
|
8
|
+
play! audio_payload(url)
|
9
|
+
end
|
10
|
+
|
11
|
+
def play_silence(duration)
|
12
|
+
play silence_payload(duration)
|
13
|
+
end
|
14
|
+
|
15
|
+
def play_silence!(duration)
|
16
|
+
play! silence_payload(duration)
|
17
|
+
end
|
18
|
+
|
19
|
+
def play_tts(sentence, language='en-US', gender='female')
|
20
|
+
play tts_payload(sentence, language, gender)
|
21
|
+
end
|
22
|
+
|
23
|
+
def play_tts!(sentence, language='en-US', gender='female')
|
24
|
+
play! tts_payload(sentence, language, gender)
|
25
|
+
end
|
26
|
+
|
27
|
+
def prompt_audio(collect, url)
|
28
|
+
prompt(collect, audio_payload(url))
|
29
|
+
end
|
30
|
+
|
31
|
+
def prompt_audio!(collect, url)
|
32
|
+
prompt!(collect, audio_payload(url))
|
33
|
+
end
|
34
|
+
|
35
|
+
def prompt_silence(collect, duration)
|
36
|
+
prompt(collect, silence_payload(duration))
|
37
|
+
end
|
38
|
+
|
39
|
+
def prompt_silence!(collect, url)
|
40
|
+
prompt!(collect, silence_payload(duration))
|
41
|
+
end
|
42
|
+
|
43
|
+
def prompt_tts(collect, sentence, language='en-US', gender='female')
|
44
|
+
prompt(collect, tts_payload(sentence, language, gender))
|
45
|
+
end
|
46
|
+
|
47
|
+
def prompt_tts!(collect, sentence, language='en-US', gender='female')
|
48
|
+
prompt!(collect, tts_payload(sentence, language, gender))
|
49
|
+
end
|
50
|
+
|
51
|
+
def wait_for_ringing
|
52
|
+
wait_for(Relay::CallState::RINGING)
|
53
|
+
end
|
54
|
+
|
55
|
+
def wait_for_answered
|
56
|
+
wait_for(Relay::CallState::ANSWERED)
|
57
|
+
end
|
58
|
+
|
59
|
+
def wait_for_ending
|
60
|
+
wait_for(Relay::CallState::ENDING)
|
61
|
+
end
|
62
|
+
|
63
|
+
def wait_for_ended
|
64
|
+
wait_for(Relay::CallState::ENDED)
|
65
|
+
end
|
66
|
+
|
67
|
+
def audio_payload(url)
|
68
|
+
[{ type: "audio", params: { url: url } }]
|
69
|
+
end
|
70
|
+
|
71
|
+
def silence_payload(duration)
|
72
|
+
[{ type: "silence", params: { duration: duration } }]
|
73
|
+
end
|
74
|
+
|
75
|
+
def tts_payload(sentence, language='en-US', gender='male')
|
76
|
+
[{ "type": 'tts', "params": { "text": sentence, "language": language, "gender": gender } }]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Signalwire::Relay::Calling
|
4
|
+
class Component
|
5
|
+
attr_reader :completed, :state, :successful, :event, :execute_result, :call
|
6
|
+
|
7
|
+
def initialize(call:)
|
8
|
+
@call = call
|
9
|
+
@completed = false
|
10
|
+
@successful = false
|
11
|
+
|
12
|
+
@call.register_component(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Relay method corresponding to the command
|
16
|
+
#
|
17
|
+
def method
|
18
|
+
raise NotImplementedError, 'Define this method on the child classes'
|
19
|
+
end
|
20
|
+
|
21
|
+
# Relay payload
|
22
|
+
# Implement this on child classes
|
23
|
+
def payload
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# The event type the subclass listens to
|
28
|
+
def event_type
|
29
|
+
raise NotImplementedError, 'Define this method on the child classes'
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute_params(method_suffix = nil)
|
33
|
+
{
|
34
|
+
protocol: @call.client.protocol,
|
35
|
+
method: method + method_suffix.to_s,
|
36
|
+
params: inner_params
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def inner_params
|
41
|
+
result = {
|
42
|
+
node_id: @call.node_id,
|
43
|
+
call_id: @call.id
|
44
|
+
}
|
45
|
+
result[:params] = payload if payload
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute
|
50
|
+
setup_handlers
|
51
|
+
@call.relay_execute execute_params do |event, outcome|
|
52
|
+
handle_execute_result(event, outcome)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def setup_handlers
|
57
|
+
@call.on :event, event_type: event_type do |evt|
|
58
|
+
notification_handler(evt)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_execute_result(event, outcome)
|
63
|
+
@execute_result = event
|
64
|
+
terminate if outcome == :failure
|
65
|
+
end
|
66
|
+
|
67
|
+
def terminate(event = nil)
|
68
|
+
@completed = true
|
69
|
+
@successful = false
|
70
|
+
@state = 'failed'
|
71
|
+
@event = event if event
|
72
|
+
blocker&.reject
|
73
|
+
end
|
74
|
+
|
75
|
+
def setup_waiting_events(events)
|
76
|
+
@events_waiting = events
|
77
|
+
end
|
78
|
+
|
79
|
+
def wait_for(*events)
|
80
|
+
setup_waiting_events(events)
|
81
|
+
execute
|
82
|
+
wait_on_blocker unless @completed
|
83
|
+
end
|
84
|
+
|
85
|
+
def wait_on_blocker
|
86
|
+
create_blocker
|
87
|
+
blocker.wait
|
88
|
+
end
|
89
|
+
|
90
|
+
# This is the most important method to implement in a subclass
|
91
|
+
#
|
92
|
+
def notification_handler(_event)
|
93
|
+
# to be implemented by subclasses. An example could be:
|
94
|
+
#
|
95
|
+
# if event.call_params[:call_state] == 'ended'
|
96
|
+
# unblock
|
97
|
+
# end
|
98
|
+
raise NotImplementedError, 'Define this method on the child classes'
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_blocker
|
102
|
+
@blocker = Concurrent::Promises.resolvable_future
|
103
|
+
end
|
104
|
+
|
105
|
+
def unblock(value)
|
106
|
+
blocker&.resolve value
|
107
|
+
end
|
108
|
+
|
109
|
+
attr_reader :blocker
|
110
|
+
|
111
|
+
def check_for_waiting_events
|
112
|
+
unblock(event) if @events_waiting.include?(@state)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|