signalwire_agents 1.1.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 +7 -0
- data/README.md +79 -0
- data/bin/swaig-test +310 -0
- data/lib/signalwire_agents/agent/agent_base.rb +1171 -0
- data/lib/signalwire_agents/contexts/context_builder.rb +622 -0
- data/lib/signalwire_agents/datamap/data_map.rb +279 -0
- data/lib/signalwire_agents/logging.rb +92 -0
- data/lib/signalwire_agents/prefabs/concierge.rb +92 -0
- data/lib/signalwire_agents/prefabs/faq_bot.rb +67 -0
- data/lib/signalwire_agents/prefabs/info_gatherer.rb +79 -0
- data/lib/signalwire_agents/prefabs/receptionist.rb +74 -0
- data/lib/signalwire_agents/prefabs/survey.rb +75 -0
- data/lib/signalwire_agents/relay/action.rb +291 -0
- data/lib/signalwire_agents/relay/call.rb +523 -0
- data/lib/signalwire_agents/relay/client.rb +671 -0
- data/lib/signalwire_agents/relay/constants.rb +124 -0
- data/lib/signalwire_agents/relay/message.rb +137 -0
- data/lib/signalwire_agents/relay/relay_event.rb +670 -0
- data/lib/signalwire_agents/rest/http_client.rb +147 -0
- data/lib/signalwire_agents/rest/namespaces/addresses.rb +19 -0
- data/lib/signalwire_agents/rest/namespaces/calling.rb +179 -0
- data/lib/signalwire_agents/rest/namespaces/chat.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/compat.rb +229 -0
- data/lib/signalwire_agents/rest/namespaces/datasphere.rb +39 -0
- data/lib/signalwire_agents/rest/namespaces/fabric.rb +175 -0
- data/lib/signalwire_agents/rest/namespaces/imported_numbers.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/logs.rb +46 -0
- data/lib/signalwire_agents/rest/namespaces/lookup.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/mfa.rb +26 -0
- data/lib/signalwire_agents/rest/namespaces/number_groups.rb +32 -0
- data/lib/signalwire_agents/rest/namespaces/phone_numbers.rb +20 -0
- data/lib/signalwire_agents/rest/namespaces/project.rb +33 -0
- data/lib/signalwire_agents/rest/namespaces/pubsub.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/queues.rb +28 -0
- data/lib/signalwire_agents/rest/namespaces/recordings.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/registry.rb +67 -0
- data/lib/signalwire_agents/rest/namespaces/short_codes.rb +26 -0
- data/lib/signalwire_agents/rest/namespaces/sip_profile.rb +22 -0
- data/lib/signalwire_agents/rest/namespaces/verified_callers.rb +24 -0
- data/lib/signalwire_agents/rest/namespaces/video.rb +129 -0
- data/lib/signalwire_agents/rest/signalwire_client.rb +110 -0
- data/lib/signalwire_agents/security/session_manager.rb +124 -0
- data/lib/signalwire_agents/server/agent_server.rb +260 -0
- data/lib/signalwire_agents/skills/builtin/api_ninjas_trivia.rb +91 -0
- data/lib/signalwire_agents/skills/builtin/claude_skills.rb +92 -0
- data/lib/signalwire_agents/skills/builtin/custom_skills.rb +54 -0
- data/lib/signalwire_agents/skills/builtin/datasphere.rb +141 -0
- data/lib/signalwire_agents/skills/builtin/datasphere_serverless.rb +107 -0
- data/lib/signalwire_agents/skills/builtin/datetime.rb +97 -0
- data/lib/signalwire_agents/skills/builtin/google_maps.rb +168 -0
- data/lib/signalwire_agents/skills/builtin/info_gatherer.rb +189 -0
- data/lib/signalwire_agents/skills/builtin/joke.rb +65 -0
- data/lib/signalwire_agents/skills/builtin/math.rb +176 -0
- data/lib/signalwire_agents/skills/builtin/mcp_gateway.rb +121 -0
- data/lib/signalwire_agents/skills/builtin/native_vector_search.rb +116 -0
- data/lib/signalwire_agents/skills/builtin/play_background_file.rb +86 -0
- data/lib/signalwire_agents/skills/builtin/spider.rb +142 -0
- data/lib/signalwire_agents/skills/builtin/swml_transfer.rb +118 -0
- data/lib/signalwire_agents/skills/builtin/weather_api.rb +85 -0
- data/lib/signalwire_agents/skills/builtin/web_search.rb +123 -0
- data/lib/signalwire_agents/skills/builtin/wikipedia_search.rb +109 -0
- data/lib/signalwire_agents/skills/skill_base.rb +58 -0
- data/lib/signalwire_agents/skills/skill_manager.rb +85 -0
- data/lib/signalwire_agents/skills/skill_registry.rb +76 -0
- data/lib/signalwire_agents/swaig/function_result.rb +777 -0
- data/lib/signalwire_agents/swml/document.rb +84 -0
- data/lib/signalwire_agents/swml/schema.json +12250 -0
- data/lib/signalwire_agents/swml/schema.rb +81 -0
- data/lib/signalwire_agents/swml/service.rb +304 -0
- data/lib/signalwire_agents/version.rb +5 -0
- data/lib/signalwire_agents.rb +19 -0
- metadata +212 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'websocket-client-simple'
|
|
6
|
+
|
|
7
|
+
module SignalWireAgents
|
|
8
|
+
module Relay
|
|
9
|
+
# Raised for RELAY JSON-RPC errors.
|
|
10
|
+
class RelayError < StandardError
|
|
11
|
+
attr_reader :code, :error_message
|
|
12
|
+
|
|
13
|
+
def initialize(code, message)
|
|
14
|
+
@code = code
|
|
15
|
+
@error_message = message
|
|
16
|
+
super("RELAY error #{code}: #{message}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# RelayClient -- WebSocket + JSON-RPC 2.0 protocol + event dispatch.
|
|
21
|
+
#
|
|
22
|
+
# One instance = one persistent WebSocket connection to SignalWire RELAY.
|
|
23
|
+
#
|
|
24
|
+
# Implements the 4 correlation mechanisms:
|
|
25
|
+
# 1. JSON-RPC id -> pending hash with ConditionVariable
|
|
26
|
+
# 2. call_id -> Call routing
|
|
27
|
+
# 3. control_id -> Action tracking per Call
|
|
28
|
+
# 4. tag -> dial correlation
|
|
29
|
+
class Client
|
|
30
|
+
attr_reader :project_id, :protocol
|
|
31
|
+
|
|
32
|
+
def initialize(project: nil, token: nil, jwt_token: nil, space: nil,
|
|
33
|
+
contexts: ['default'])
|
|
34
|
+
@project_id = project || ENV['SIGNALWIRE_PROJECT_ID'] || ''
|
|
35
|
+
@token = token || ENV['SIGNALWIRE_API_TOKEN'] || ''
|
|
36
|
+
@jwt_token = jwt_token
|
|
37
|
+
@space = space || ENV['SIGNALWIRE_SPACE'] || ''
|
|
38
|
+
@contexts = contexts
|
|
39
|
+
|
|
40
|
+
raise ArgumentError, 'project is required' if @project_id.empty?
|
|
41
|
+
if @token.empty? && @jwt_token.nil?
|
|
42
|
+
raise ArgumentError, 'token or jwt_token is required'
|
|
43
|
+
end
|
|
44
|
+
raise ArgumentError, 'space is required' if @space.empty?
|
|
45
|
+
|
|
46
|
+
@host = @space.include?('.') ? @space : "#{@space}.signalwire.com"
|
|
47
|
+
|
|
48
|
+
# Correlation mechanisms
|
|
49
|
+
@pending = {} # id -> { mutex:, cv:, result:, error: }
|
|
50
|
+
@pending_mutex = Mutex.new
|
|
51
|
+
@calls = {} # call_id -> Call
|
|
52
|
+
@calls_mutex = Mutex.new
|
|
53
|
+
@pending_dials = {} # tag -> { mutex:, cv:, call:, error: }
|
|
54
|
+
@dials_mutex = Mutex.new
|
|
55
|
+
@messages = {} # message_id -> Message
|
|
56
|
+
@messages_mutex = Mutex.new
|
|
57
|
+
|
|
58
|
+
# Session state
|
|
59
|
+
@protocol = nil
|
|
60
|
+
@authorization_state = nil
|
|
61
|
+
@ws = nil
|
|
62
|
+
@running = false
|
|
63
|
+
@connected = false
|
|
64
|
+
@ws_mutex = Mutex.new
|
|
65
|
+
|
|
66
|
+
# Handlers
|
|
67
|
+
@on_call_handler = nil
|
|
68
|
+
@on_message_handler = nil
|
|
69
|
+
|
|
70
|
+
# Reconnection
|
|
71
|
+
@reconnect_delay = RECONNECT_MIN_DELAY
|
|
72
|
+
@should_restart = false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Register inbound call handler.
|
|
76
|
+
def on_call(&block)
|
|
77
|
+
@on_call_handler = block
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Register inbound message handler.
|
|
81
|
+
def on_message(&block)
|
|
82
|
+
@on_message_handler = block
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Connect, authenticate, subscribe, and enter the read loop.
|
|
86
|
+
# Blocks until stop is called.
|
|
87
|
+
def run
|
|
88
|
+
@running = true
|
|
89
|
+
while @running
|
|
90
|
+
begin
|
|
91
|
+
_connect_and_run
|
|
92
|
+
rescue => e
|
|
93
|
+
$stderr.puts "[RELAY] Connection error: #{e.message}"
|
|
94
|
+
end
|
|
95
|
+
break unless @running
|
|
96
|
+
|
|
97
|
+
# Reject all pending requests
|
|
98
|
+
_reject_all_pending('Disconnected')
|
|
99
|
+
|
|
100
|
+
# Exponential backoff reconnect
|
|
101
|
+
$stderr.puts "[RELAY] Reconnecting in #{@reconnect_delay}s..."
|
|
102
|
+
sleep(@reconnect_delay)
|
|
103
|
+
@reconnect_delay = [
|
|
104
|
+
@reconnect_delay * RECONNECT_BACKOFF_FACTOR,
|
|
105
|
+
RECONNECT_MAX_DELAY
|
|
106
|
+
].min
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Graceful shutdown.
|
|
111
|
+
def stop
|
|
112
|
+
@running = false
|
|
113
|
+
@ws_mutex.synchronize do
|
|
114
|
+
@ws&.close if @connected
|
|
115
|
+
end
|
|
116
|
+
_reject_all_pending('Client stopped')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
# Outbound dial
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
# Dial outbound call(s). Returns a Call object.
|
|
124
|
+
def dial(devices, timeout: 120, tag: nil, **kwargs)
|
|
125
|
+
dial_tag = tag || SecureRandom.uuid
|
|
126
|
+
|
|
127
|
+
# Register pending dial BEFORE sending RPC
|
|
128
|
+
entry = { mutex: Mutex.new, cv: ConditionVariable.new, call: nil, error: nil }
|
|
129
|
+
@dials_mutex.synchronize { @pending_dials[dial_tag] = entry }
|
|
130
|
+
|
|
131
|
+
begin
|
|
132
|
+
params = { 'tag' => dial_tag, 'devices' => devices }
|
|
133
|
+
kwargs.each { |k, v| params[k.to_s] = v }
|
|
134
|
+
execute('calling.dial', params)
|
|
135
|
+
rescue => e
|
|
136
|
+
@dials_mutex.synchronize { @pending_dials.delete(dial_tag) }
|
|
137
|
+
raise
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Wait for calling.call.dial event
|
|
141
|
+
entry[:mutex].synchronize do
|
|
142
|
+
deadline = Time.now + timeout
|
|
143
|
+
while entry[:call].nil? && entry[:error].nil?
|
|
144
|
+
remaining = deadline - Time.now
|
|
145
|
+
if remaining <= 0
|
|
146
|
+
@dials_mutex.synchronize { @pending_dials.delete(dial_tag) }
|
|
147
|
+
raise ActionTimeoutError, "Dial timed out after #{timeout}s"
|
|
148
|
+
end
|
|
149
|
+
entry[:cv].wait(entry[:mutex], remaining)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
@dials_mutex.synchronize { @pending_dials.delete(dial_tag) }
|
|
154
|
+
raise RelayError.new(-1, entry[:error]) if entry[:error]
|
|
155
|
+
entry[:call]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
# Outbound message
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
# Send an SMS/MMS message. Returns a Message object.
|
|
163
|
+
def send_message(to:, from:, body: nil, media: nil, context: nil,
|
|
164
|
+
tags: nil, on_completed: nil, **kwargs)
|
|
165
|
+
raise ArgumentError, 'body or media is required' if (body.nil? || body.empty?) && (media.nil? || media.empty?)
|
|
166
|
+
|
|
167
|
+
msg_context = context || @contexts.first || 'default'
|
|
168
|
+
params = {
|
|
169
|
+
'context' => msg_context,
|
|
170
|
+
'to_number' => to,
|
|
171
|
+
'from_number' => from
|
|
172
|
+
}
|
|
173
|
+
params['body'] = body if body
|
|
174
|
+
params['media'] = media if media
|
|
175
|
+
params['tags'] = tags if tags
|
|
176
|
+
kwargs.each { |k, v| params[k.to_s] = v }
|
|
177
|
+
|
|
178
|
+
result = execute('messaging.send', params)
|
|
179
|
+
message_id = result['message_id'] || ''
|
|
180
|
+
|
|
181
|
+
msg = Message.new(
|
|
182
|
+
message_id: message_id,
|
|
183
|
+
context: msg_context,
|
|
184
|
+
direction: 'outbound',
|
|
185
|
+
from_number: from,
|
|
186
|
+
to_number: to,
|
|
187
|
+
body: body || '',
|
|
188
|
+
media: media || [],
|
|
189
|
+
state: 'queued',
|
|
190
|
+
tags: tags || []
|
|
191
|
+
)
|
|
192
|
+
msg._set_on_completed(on_completed) if on_completed
|
|
193
|
+
@messages_mutex.synchronize { @messages[message_id] = msg } unless message_id.empty?
|
|
194
|
+
msg
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
# Dynamic context subscription
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def receive(contexts)
|
|
202
|
+
execute('signalwire.receive', { 'contexts' => contexts })
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def unreceive(contexts)
|
|
206
|
+
execute('signalwire.unreceive', { 'contexts' => contexts })
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
# JSON-RPC execute
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
# Send a JSON-RPC request and wait for the response.
|
|
214
|
+
# Returns the result hash. Raises RelayError on error.
|
|
215
|
+
def execute(method, params = {})
|
|
216
|
+
id = SecureRandom.uuid
|
|
217
|
+
|
|
218
|
+
# Add protocol to params if we have one (except for signalwire.connect)
|
|
219
|
+
if @protocol && method != METHOD_SIGNALWIRE_CONNECT
|
|
220
|
+
params = params.dup
|
|
221
|
+
params['protocol'] = @protocol
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
msg = {
|
|
225
|
+
'jsonrpc' => '2.0',
|
|
226
|
+
'id' => id,
|
|
227
|
+
'method' => method,
|
|
228
|
+
'params' => params
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
entry = { mutex: Mutex.new, cv: ConditionVariable.new, result: nil, error: nil }
|
|
232
|
+
@pending_mutex.synchronize { @pending[id] = entry }
|
|
233
|
+
|
|
234
|
+
_send_json(msg)
|
|
235
|
+
|
|
236
|
+
# Wait for response (10s timeout to detect half-open connections)
|
|
237
|
+
entry[:mutex].synchronize do
|
|
238
|
+
deadline = Time.now + 10
|
|
239
|
+
while entry[:result].nil? && entry[:error].nil?
|
|
240
|
+
remaining = deadline - Time.now
|
|
241
|
+
if remaining <= 0
|
|
242
|
+
@pending_mutex.synchronize { @pending.delete(id) }
|
|
243
|
+
raise RelayError.new(-1, "Request #{method} timed out")
|
|
244
|
+
end
|
|
245
|
+
entry[:cv].wait(entry[:mutex], remaining)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
@pending_mutex.synchronize { @pending.delete(id) }
|
|
250
|
+
raise entry[:error] if entry[:error]
|
|
251
|
+
|
|
252
|
+
result = entry[:result]
|
|
253
|
+
|
|
254
|
+
# Check result code for non-connect methods
|
|
255
|
+
if method != METHOD_SIGNALWIRE_CONNECT
|
|
256
|
+
code = result['code']
|
|
257
|
+
if code && !code.to_s.match?(/\A2\d\d\z/)
|
|
258
|
+
raise RelayError.new(code, result['message'] || 'Unknown error')
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
result
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
private
|
|
266
|
+
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
# WebSocket connection lifecycle
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
def _connect_and_run
|
|
272
|
+
url = "wss://#{@host}"
|
|
273
|
+
ready_mutex = Mutex.new
|
|
274
|
+
ready_cv = ConditionVariable.new
|
|
275
|
+
ready_flag = false
|
|
276
|
+
ws_error = nil
|
|
277
|
+
|
|
278
|
+
client_ref = self
|
|
279
|
+
|
|
280
|
+
@ws = WebSocket::Client::Simple.connect(url) do |ws|
|
|
281
|
+
ws.on :open do
|
|
282
|
+
client_ref.send(:_on_ws_open)
|
|
283
|
+
ready_mutex.synchronize do
|
|
284
|
+
ready_flag = true
|
|
285
|
+
ready_cv.signal
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
ws.on :message do |msg|
|
|
290
|
+
client_ref.send(:_on_ws_message, msg.data)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
ws.on :error do |e|
|
|
294
|
+
ws_error = e
|
|
295
|
+
ready_mutex.synchronize do
|
|
296
|
+
ready_flag = true
|
|
297
|
+
ready_cv.signal
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
ws.on :close do |_e|
|
|
302
|
+
client_ref.send(:_on_ws_close)
|
|
303
|
+
ready_mutex.synchronize do
|
|
304
|
+
ready_flag = true
|
|
305
|
+
ready_cv.signal
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Wait for connection to open
|
|
311
|
+
ready_mutex.synchronize do
|
|
312
|
+
ready_cv.wait(ready_mutex, 15) until ready_flag
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
raise ws_error if ws_error
|
|
316
|
+
|
|
317
|
+
@ws_mutex.synchronize { @connected = true }
|
|
318
|
+
@reconnect_delay = RECONNECT_MIN_DELAY
|
|
319
|
+
|
|
320
|
+
# Authenticate
|
|
321
|
+
_authenticate
|
|
322
|
+
|
|
323
|
+
# Keep reading until disconnected
|
|
324
|
+
while @running && @connected
|
|
325
|
+
sleep 1
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def _on_ws_open
|
|
330
|
+
# Connection opened
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def _on_ws_message(data)
|
|
334
|
+
return if data.nil? || data.empty?
|
|
335
|
+
|
|
336
|
+
begin
|
|
337
|
+
msg = JSON.parse(data)
|
|
338
|
+
rescue JSON::ParserError => e
|
|
339
|
+
$stderr.puts "[RELAY] Failed to parse message: #{e.message}"
|
|
340
|
+
return
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
_handle_message(msg)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def _on_ws_close
|
|
347
|
+
@ws_mutex.synchronize { @connected = false }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def _send_json(msg)
|
|
351
|
+
@ws_mutex.synchronize do
|
|
352
|
+
return unless @ws && @connected
|
|
353
|
+
|
|
354
|
+
@ws.send(JSON.generate(msg))
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# ------------------------------------------------------------------
|
|
359
|
+
# Authentication
|
|
360
|
+
# ------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
def _authenticate
|
|
363
|
+
params = {
|
|
364
|
+
'version' => PROTOCOL_VERSION,
|
|
365
|
+
'agent' => AGENT_STRING,
|
|
366
|
+
'event_acks' => true
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if @jwt_token
|
|
370
|
+
params['authentication'] = { 'jwt_token' => @jwt_token }
|
|
371
|
+
else
|
|
372
|
+
params['authentication'] = {
|
|
373
|
+
'project' => @project_id,
|
|
374
|
+
'token' => @token
|
|
375
|
+
}
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
params['contexts'] = @contexts unless @contexts.empty?
|
|
379
|
+
params['protocol'] = @protocol if @protocol && !@should_restart
|
|
380
|
+
params['authorization_state'] = @authorization_state if @authorization_state && !@should_restart
|
|
381
|
+
|
|
382
|
+
if @should_restart
|
|
383
|
+
@protocol = nil
|
|
384
|
+
@authorization_state = nil
|
|
385
|
+
@should_restart = false
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
result = execute(METHOD_SIGNALWIRE_CONNECT, params)
|
|
389
|
+
@protocol = result['protocol'] if result['protocol']
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# ------------------------------------------------------------------
|
|
393
|
+
# Message dispatch
|
|
394
|
+
# ------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
def _handle_message(msg)
|
|
397
|
+
method = msg['method']
|
|
398
|
+
id = msg['id']
|
|
399
|
+
|
|
400
|
+
if method.nil?
|
|
401
|
+
# This is a response to a pending request
|
|
402
|
+
_handle_response(msg)
|
|
403
|
+
return
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
case method
|
|
407
|
+
when METHOD_SIGNALWIRE_EVENT
|
|
408
|
+
_handle_event(msg)
|
|
409
|
+
when METHOD_SIGNALWIRE_PING
|
|
410
|
+
_send_json({ 'jsonrpc' => '2.0', 'id' => id, 'result' => {} })
|
|
411
|
+
when METHOD_SIGNALWIRE_DISCONNECT
|
|
412
|
+
_handle_disconnect(msg)
|
|
413
|
+
else
|
|
414
|
+
# Unknown method, send empty result
|
|
415
|
+
_send_json({ 'jsonrpc' => '2.0', 'id' => id, 'result' => {} }) if id
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def _handle_response(msg)
|
|
420
|
+
id = msg['id']
|
|
421
|
+
return unless id
|
|
422
|
+
|
|
423
|
+
entry = @pending_mutex.synchronize { @pending[id] }
|
|
424
|
+
return unless entry
|
|
425
|
+
|
|
426
|
+
if msg['error']
|
|
427
|
+
err = msg['error']
|
|
428
|
+
entry[:mutex].synchronize do
|
|
429
|
+
entry[:error] = RelayError.new(err['code'], err['message'] || 'Unknown error')
|
|
430
|
+
entry[:cv].signal
|
|
431
|
+
end
|
|
432
|
+
else
|
|
433
|
+
entry[:mutex].synchronize do
|
|
434
|
+
entry[:result] = msg['result'] || {}
|
|
435
|
+
entry[:cv].signal
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def _handle_event(msg)
|
|
441
|
+
id = msg['id']
|
|
442
|
+
outer_params = msg['params'] || {}
|
|
443
|
+
|
|
444
|
+
# ACK the event immediately
|
|
445
|
+
_send_json({ 'jsonrpc' => '2.0', 'id' => id, 'result' => {} }) if id
|
|
446
|
+
|
|
447
|
+
event_type = outer_params['event_type'] || ''
|
|
448
|
+
event_params = outer_params['params'] || {}
|
|
449
|
+
call_id = event_params['call_id'] || ''
|
|
450
|
+
|
|
451
|
+
# Authorization state
|
|
452
|
+
if event_type == EVENT_AUTHORIZATION_STATE
|
|
453
|
+
@authorization_state = event_params['authorization_state']
|
|
454
|
+
return
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Inbound call
|
|
458
|
+
if event_type == EVENT_CALL_RECEIVE
|
|
459
|
+
_handle_inbound_call(outer_params)
|
|
460
|
+
return
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Dial completion -- call_id is NESTED at params.call.call_id
|
|
464
|
+
if event_type == EVENT_CALL_DIAL
|
|
465
|
+
_handle_dial_event(outer_params)
|
|
466
|
+
return
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Inbound message
|
|
470
|
+
if event_type == EVENT_MESSAGING_RECEIVE
|
|
471
|
+
_handle_inbound_message(outer_params)
|
|
472
|
+
return
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Outbound message state
|
|
476
|
+
if event_type == EVENT_MESSAGING_STATE
|
|
477
|
+
_handle_message_state(outer_params)
|
|
478
|
+
return
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# State events during dial -- call not registered yet
|
|
482
|
+
if event_type == EVENT_CALL_STATE
|
|
483
|
+
tag = event_params['tag'] || ''
|
|
484
|
+
has_pending = @dials_mutex.synchronize { @pending_dials.key?(tag) }
|
|
485
|
+
if !tag.empty? && has_pending
|
|
486
|
+
has_call = @calls_mutex.synchronize { @calls.key?(call_id) }
|
|
487
|
+
unless has_call || call_id.empty?
|
|
488
|
+
_register_dial_leg(tag, event_params)
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
# Fall through to normal routing
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Normal routing by call_id
|
|
495
|
+
unless call_id.empty?
|
|
496
|
+
call = @calls_mutex.synchronize { @calls[call_id] }
|
|
497
|
+
if call
|
|
498
|
+
call._dispatch_event(outer_params)
|
|
499
|
+
if call.state == CALL_STATE_ENDED
|
|
500
|
+
@calls_mutex.synchronize { @calls.delete(call_id) }
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def _handle_disconnect(msg)
|
|
507
|
+
id = msg['id']
|
|
508
|
+
params = msg['params'] || {}
|
|
509
|
+
|
|
510
|
+
# Respond with empty result
|
|
511
|
+
_send_json({ 'jsonrpc' => '2.0', 'id' => id, 'result' => {} }) if id
|
|
512
|
+
|
|
513
|
+
# Check restart flag
|
|
514
|
+
@should_restart = params['restart'] == true
|
|
515
|
+
|
|
516
|
+
# Let the connection close, reconnect will happen automatically
|
|
517
|
+
@ws_mutex.synchronize { @connected = false }
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def _handle_inbound_call(payload)
|
|
521
|
+
event_params = payload['params'] || {}
|
|
522
|
+
call = Call.new(
|
|
523
|
+
self,
|
|
524
|
+
call_id: event_params['call_id'] || '',
|
|
525
|
+
node_id: event_params['node_id'] || '',
|
|
526
|
+
project_id: event_params['project_id'] || '',
|
|
527
|
+
context: event_params['context'] || event_params['protocol'] || '',
|
|
528
|
+
tag: event_params['tag'] || '',
|
|
529
|
+
direction: event_params['direction'] || 'inbound',
|
|
530
|
+
device: event_params['device'] || {},
|
|
531
|
+
state: event_params['call_state'] || '',
|
|
532
|
+
segment_id: event_params['segment_id'] || ''
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
@calls_mutex.synchronize { @calls[call.call_id] = call }
|
|
536
|
+
|
|
537
|
+
if @on_call_handler
|
|
538
|
+
Thread.new do
|
|
539
|
+
begin
|
|
540
|
+
@on_call_handler.call(call)
|
|
541
|
+
rescue => e
|
|
542
|
+
$stderr.puts "[RELAY] Error in on_call handler: #{e.message}"
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def _handle_dial_event(payload)
|
|
549
|
+
event_params = payload['params'] || {}
|
|
550
|
+
tag = event_params['tag'] || ''
|
|
551
|
+
dial_state = event_params['dial_state'] || ''
|
|
552
|
+
call_info = event_params['call'] || {}
|
|
553
|
+
|
|
554
|
+
entry = @dials_mutex.synchronize { @pending_dials[tag] }
|
|
555
|
+
return unless entry
|
|
556
|
+
|
|
557
|
+
if dial_state == 'answered'
|
|
558
|
+
call_id = call_info['call_id'] || ''
|
|
559
|
+
node_id = call_info['node_id'] || ''
|
|
560
|
+
|
|
561
|
+
# Find or create the call
|
|
562
|
+
call = @calls_mutex.synchronize { @calls[call_id] }
|
|
563
|
+
unless call
|
|
564
|
+
call = Call.new(
|
|
565
|
+
self,
|
|
566
|
+
call_id: call_id,
|
|
567
|
+
node_id: node_id,
|
|
568
|
+
project_id: @project_id,
|
|
569
|
+
tag: call_info['tag'] || tag,
|
|
570
|
+
direction: 'outbound',
|
|
571
|
+
device: call_info['device'] || {},
|
|
572
|
+
state: CALL_STATE_ANSWERED
|
|
573
|
+
)
|
|
574
|
+
@calls_mutex.synchronize { @calls[call_id] = call }
|
|
575
|
+
end
|
|
576
|
+
call.state = CALL_STATE_ANSWERED
|
|
577
|
+
|
|
578
|
+
entry[:mutex].synchronize do
|
|
579
|
+
entry[:call] = call
|
|
580
|
+
entry[:cv].signal
|
|
581
|
+
end
|
|
582
|
+
elsif dial_state == 'failed'
|
|
583
|
+
entry[:mutex].synchronize do
|
|
584
|
+
entry[:error] = 'Dial failed'
|
|
585
|
+
entry[:cv].signal
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def _register_dial_leg(tag, event_params)
|
|
591
|
+
call_id = event_params['call_id'] || ''
|
|
592
|
+
return if call_id.empty?
|
|
593
|
+
|
|
594
|
+
call = Call.new(
|
|
595
|
+
self,
|
|
596
|
+
call_id: call_id,
|
|
597
|
+
node_id: event_params['node_id'] || '',
|
|
598
|
+
project_id: @project_id,
|
|
599
|
+
tag: tag,
|
|
600
|
+
direction: 'outbound',
|
|
601
|
+
device: event_params['device'] || {},
|
|
602
|
+
state: event_params['call_state'] || ''
|
|
603
|
+
)
|
|
604
|
+
@calls_mutex.synchronize { @calls[call_id] = call }
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def _handle_inbound_message(payload)
|
|
608
|
+
event_params = payload['params'] || {}
|
|
609
|
+
msg = Message.new(
|
|
610
|
+
message_id: event_params['message_id'] || '',
|
|
611
|
+
context: event_params['context'] || '',
|
|
612
|
+
direction: 'inbound',
|
|
613
|
+
from_number: event_params['from_number'] || '',
|
|
614
|
+
to_number: event_params['to_number'] || '',
|
|
615
|
+
body: event_params['body'] || '',
|
|
616
|
+
media: event_params['media'] || [],
|
|
617
|
+
segments: event_params['segments'] || 0,
|
|
618
|
+
state: event_params['message_state'] || 'received',
|
|
619
|
+
tags: event_params['tags'] || []
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
if @on_message_handler
|
|
623
|
+
Thread.new do
|
|
624
|
+
begin
|
|
625
|
+
@on_message_handler.call(msg)
|
|
626
|
+
rescue => e
|
|
627
|
+
$stderr.puts "[RELAY] Error in on_message handler: #{e.message}"
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def _handle_message_state(payload)
|
|
634
|
+
event_params = payload['params'] || {}
|
|
635
|
+
message_id = event_params['message_id'] || ''
|
|
636
|
+
|
|
637
|
+
msg = @messages_mutex.synchronize { @messages[message_id] }
|
|
638
|
+
return unless msg
|
|
639
|
+
|
|
640
|
+
msg._dispatch_event(payload)
|
|
641
|
+
|
|
642
|
+
# Clean up terminal messages
|
|
643
|
+
if msg.done?
|
|
644
|
+
@messages_mutex.synchronize { @messages.delete(message_id) }
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def _reject_all_pending(reason)
|
|
649
|
+
@pending_mutex.synchronize do
|
|
650
|
+
@pending.each_value do |entry|
|
|
651
|
+
entry[:mutex].synchronize do
|
|
652
|
+
entry[:error] ||= RelayError.new(-1, reason)
|
|
653
|
+
entry[:cv].signal
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
@pending.clear
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
@dials_mutex.synchronize do
|
|
660
|
+
@pending_dials.each_value do |entry|
|
|
661
|
+
entry[:mutex].synchronize do
|
|
662
|
+
entry[:error] ||= reason
|
|
663
|
+
entry[:cv].signal
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
@pending_dials.clear
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
end
|