pbx 0.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/LICENSE +21 -0
- data/README.md +120 -0
- data/examples/pbx.example.yml +10 -0
- data/exe/pbx +6 -0
- data/lib/pbx/ami_bridge.rb +456 -0
- data/lib/pbx/app.rb +376 -0
- data/lib/pbx/call.rb +9 -0
- data/lib/pbx/call_queue.rb +9 -0
- data/lib/pbx/cli.rb +46 -0
- data/lib/pbx/config.rb +45 -0
- data/lib/pbx/messages.rb +205 -0
- data/lib/pbx/peer.rb +5 -0
- data/lib/pbx/queue_member.rb +7 -0
- data/lib/pbx/status.rb +84 -0
- data/lib/pbx/version.rb +5 -0
- data/lib/pbx/views/active_calls.rb +101 -0
- data/lib/pbx/views/disconnected_screen.rb +60 -0
- data/lib/pbx/views/extension_table.rb +84 -0
- data/lib/pbx/views/footer.rb +51 -0
- data/lib/pbx/views/header.rb +63 -0
- data/lib/pbx/views/info_modal.rb +75 -0
- data/lib/pbx/views/queue_table.rb +62 -0
- data/lib/pbx.rb +20 -0
- metadata +205 -0
data/lib/pbx/app.rb
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bubbletea"
|
|
4
|
+
require "bubbles"
|
|
5
|
+
require "lipgloss"
|
|
6
|
+
require_relative "messages"
|
|
7
|
+
require_relative "views/header"
|
|
8
|
+
require_relative "views/extension_table"
|
|
9
|
+
require_relative "views/active_calls"
|
|
10
|
+
require_relative "views/queue_table"
|
|
11
|
+
require_relative "views/footer"
|
|
12
|
+
require_relative "views/disconnected_screen"
|
|
13
|
+
require_relative "views/info_modal"
|
|
14
|
+
|
|
15
|
+
module Pbx
|
|
16
|
+
class App
|
|
17
|
+
include Bubbletea::Model
|
|
18
|
+
|
|
19
|
+
TICK_INTERVAL = 1 # seconds between "time since last change" refresh
|
|
20
|
+
|
|
21
|
+
attr_reader :extensions, :active_calls, :queues, :status, :error, :width, :height, :config,
|
|
22
|
+
:show_info, :system_boot_at, :last_reload_at, :view_mode
|
|
23
|
+
|
|
24
|
+
def spinner_view = @spinner.view
|
|
25
|
+
|
|
26
|
+
def initialize(bridge:, config:)
|
|
27
|
+
@bridge = bridge
|
|
28
|
+
@config = config
|
|
29
|
+
@extensions = {}
|
|
30
|
+
@active_calls = {}
|
|
31
|
+
@queues = {}
|
|
32
|
+
@status = @config.complete? ? :connecting : :disconnected
|
|
33
|
+
@error = nil
|
|
34
|
+
@width = 80
|
|
35
|
+
@height = 24
|
|
36
|
+
@table = nil
|
|
37
|
+
@calls_table = nil
|
|
38
|
+
@queues_table = nil
|
|
39
|
+
@show_info = false
|
|
40
|
+
@view_mode = :peers
|
|
41
|
+
@spinner = Bubbles::Spinner.new(spinner: Bubbles::Spinners::DOT)
|
|
42
|
+
@system_boot_at = nil
|
|
43
|
+
@last_reload_at = nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def init
|
|
47
|
+
if @config.complete?
|
|
48
|
+
@spinner, spinner_cmd = @spinner.init
|
|
49
|
+
[self, Bubbletea.batch(connect_cmd, tick_cmd, spinner_cmd)]
|
|
50
|
+
else
|
|
51
|
+
[self, tick_cmd]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def update(message)
|
|
56
|
+
case message
|
|
57
|
+
when Bubbletea::KeyMessage
|
|
58
|
+
if @show_info
|
|
59
|
+
@show_info = false
|
|
60
|
+
return [self, nil]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
return [self, Bubbletea.quit] if quit_key?(message)
|
|
64
|
+
|
|
65
|
+
if message.runes? && message.char == "i"
|
|
66
|
+
@show_info = true
|
|
67
|
+
return [self, nil]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if message.runes? && message.char == "p" && @status == :connected
|
|
71
|
+
@view_mode = :peers
|
|
72
|
+
rebuild_table
|
|
73
|
+
return [self, nil]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if message.runes? && message.char == "c" && @status == :connected
|
|
77
|
+
@view_mode = :calls
|
|
78
|
+
rebuild_table
|
|
79
|
+
return [self, nil]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if message.runes? && message.char == "q" && @status == :connected
|
|
83
|
+
@view_mode = :queues
|
|
84
|
+
return [self, nil]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
active_table = case @view_mode
|
|
88
|
+
when :calls then @calls_table
|
|
89
|
+
when :queues then @queues_table
|
|
90
|
+
else @table
|
|
91
|
+
end
|
|
92
|
+
if active_table && @status == :connected
|
|
93
|
+
updated, table_cmd = active_table.update(message)
|
|
94
|
+
case @view_mode
|
|
95
|
+
when :calls then @calls_table = updated
|
|
96
|
+
when :queues then @queues_table = updated
|
|
97
|
+
else @table = updated
|
|
98
|
+
end
|
|
99
|
+
return [self, table_cmd]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
when Bubbletea::WindowSizeMessage
|
|
103
|
+
@width = message.width
|
|
104
|
+
@height = message.height
|
|
105
|
+
if @status == :connected
|
|
106
|
+
rebuild_table
|
|
107
|
+
rebuild_calls_table
|
|
108
|
+
rebuild_queues_table
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
when Messages::ConnectionEstablished
|
|
112
|
+
@status = :connected
|
|
113
|
+
@error = nil
|
|
114
|
+
message.peers.each { |p| @extensions[p.id] = p }
|
|
115
|
+
@queues = message.queues
|
|
116
|
+
rebuild_table
|
|
117
|
+
rebuild_queues_table
|
|
118
|
+
return [self, wait_for_event_cmd]
|
|
119
|
+
|
|
120
|
+
when Messages::ConnectionLost
|
|
121
|
+
@status = :lost
|
|
122
|
+
@error = message.reason
|
|
123
|
+
return [self, wait_for_event_cmd]
|
|
124
|
+
|
|
125
|
+
when Messages::PeerDiscovered
|
|
126
|
+
@extensions[message.peer.id] = message.peer
|
|
127
|
+
rebuild_table
|
|
128
|
+
return [self, wait_for_event_cmd]
|
|
129
|
+
|
|
130
|
+
when Messages::PeerStatusChanged
|
|
131
|
+
if (peer = @extensions[message.peer_name])
|
|
132
|
+
@extensions[peer.id] = Peer.new(
|
|
133
|
+
id: peer.id,
|
|
134
|
+
name: peer.name,
|
|
135
|
+
ip_address: message.ip_address || peer.ip_address,
|
|
136
|
+
ip_port: message.ip_port || peer.ip_port,
|
|
137
|
+
status: message.status,
|
|
138
|
+
type: peer.type,
|
|
139
|
+
dynamic: peer.dynamic,
|
|
140
|
+
user_agent: peer.user_agent,
|
|
141
|
+
rtt_ms: message.rtt_ms || peer.rtt_ms,
|
|
142
|
+
last_change_at: message.at
|
|
143
|
+
)
|
|
144
|
+
rebuild_table
|
|
145
|
+
end
|
|
146
|
+
return [self, wait_for_event_cmd]
|
|
147
|
+
|
|
148
|
+
when Messages::CallStarted
|
|
149
|
+
@active_calls[message.uniqueid] = Call.new(
|
|
150
|
+
uniqueid: message.uniqueid,
|
|
151
|
+
channel: message.channel,
|
|
152
|
+
caller_id: message.caller_id,
|
|
153
|
+
caller_name: message.caller_name,
|
|
154
|
+
connected_to: nil,
|
|
155
|
+
state: message.state,
|
|
156
|
+
started_at: message.started_at,
|
|
157
|
+
outcome: nil,
|
|
158
|
+
held: false,
|
|
159
|
+
dialplan_app: nil,
|
|
160
|
+
dialplan_exten: nil
|
|
161
|
+
)
|
|
162
|
+
rebuild_table
|
|
163
|
+
rebuild_calls_table
|
|
164
|
+
return [self, wait_for_event_cmd]
|
|
165
|
+
|
|
166
|
+
when Messages::CallEnded
|
|
167
|
+
@active_calls.delete(message.uniqueid)
|
|
168
|
+
rebuild_table
|
|
169
|
+
rebuild_calls_table
|
|
170
|
+
return [self, wait_for_event_cmd]
|
|
171
|
+
|
|
172
|
+
when Messages::CallStateChanged
|
|
173
|
+
if (call = @active_calls[message.uniqueid])
|
|
174
|
+
@active_calls[call.uniqueid] = call.with(
|
|
175
|
+
connected_to: message.connected_to || call.connected_to,
|
|
176
|
+
state: message.state
|
|
177
|
+
)
|
|
178
|
+
rebuild_table
|
|
179
|
+
rebuild_calls_table
|
|
180
|
+
end
|
|
181
|
+
return [self, wait_for_event_cmd]
|
|
182
|
+
|
|
183
|
+
when Messages::DialCompleted
|
|
184
|
+
if (call = @active_calls[message.uniqueid])
|
|
185
|
+
@active_calls[call.uniqueid] = call.with(outcome: message.dial_status)
|
|
186
|
+
rebuild_table
|
|
187
|
+
rebuild_calls_table
|
|
188
|
+
end
|
|
189
|
+
return [self, wait_for_event_cmd]
|
|
190
|
+
|
|
191
|
+
when Messages::CallHeld
|
|
192
|
+
if (call = @active_calls[message.uniqueid])
|
|
193
|
+
@active_calls[call.uniqueid] = call.with(held: true)
|
|
194
|
+
rebuild_table
|
|
195
|
+
rebuild_calls_table
|
|
196
|
+
end
|
|
197
|
+
return [self, wait_for_event_cmd]
|
|
198
|
+
|
|
199
|
+
when Messages::CallUnheld
|
|
200
|
+
if (call = @active_calls[message.uniqueid])
|
|
201
|
+
@active_calls[call.uniqueid] = call.with(held: false)
|
|
202
|
+
rebuild_table
|
|
203
|
+
rebuild_calls_table
|
|
204
|
+
end
|
|
205
|
+
return [self, wait_for_event_cmd]
|
|
206
|
+
|
|
207
|
+
when Messages::CallDialplanUpdate
|
|
208
|
+
if (call = @active_calls[message.uniqueid])
|
|
209
|
+
@active_calls[call.uniqueid] = call.with(
|
|
210
|
+
dialplan_app: message.application,
|
|
211
|
+
dialplan_exten: message.exten
|
|
212
|
+
)
|
|
213
|
+
rebuild_table
|
|
214
|
+
rebuild_calls_table
|
|
215
|
+
end
|
|
216
|
+
return [self, wait_for_event_cmd]
|
|
217
|
+
|
|
218
|
+
when Messages::QueueCallCompleted
|
|
219
|
+
if (q = @queues[message.queue])
|
|
220
|
+
@queues[message.queue] = q.with(
|
|
221
|
+
completed: q.completed + 1,
|
|
222
|
+
last_holdtime: message.holdtime
|
|
223
|
+
)
|
|
224
|
+
rebuild_queues_table
|
|
225
|
+
end
|
|
226
|
+
return [self, wait_for_event_cmd]
|
|
227
|
+
|
|
228
|
+
when Messages::QueueCallerCountChanged
|
|
229
|
+
if (q = @queues[message.queue])
|
|
230
|
+
@queues[message.queue] = q.with(calls_waiting: message.count)
|
|
231
|
+
rebuild_queues_table
|
|
232
|
+
end
|
|
233
|
+
return [self, wait_for_event_cmd]
|
|
234
|
+
|
|
235
|
+
when Messages::QueueCallerAbandoned
|
|
236
|
+
if (q = @queues[message.queue])
|
|
237
|
+
@queues[message.queue] = q.with(abandoned: q.abandoned + 1)
|
|
238
|
+
rebuild_queues_table
|
|
239
|
+
end
|
|
240
|
+
return [self, wait_for_event_cmd]
|
|
241
|
+
|
|
242
|
+
when Messages::QueueMemberUpdated
|
|
243
|
+
q = @queues[message.queue] || CallQueue.new(
|
|
244
|
+
name: message.queue, strategy: "unknown", calls_waiting: 0,
|
|
245
|
+
completed: 0, abandoned: 0, holdtime: 0, members: {}
|
|
246
|
+
)
|
|
247
|
+
existing = q.members[message.interface]
|
|
248
|
+
updated_member = if existing
|
|
249
|
+
existing.with(
|
|
250
|
+
name: message.name.empty? ? existing.name : message.name,
|
|
251
|
+
status: message.status || existing.status,
|
|
252
|
+
paused: message.paused
|
|
253
|
+
)
|
|
254
|
+
else
|
|
255
|
+
QueueMember.new(
|
|
256
|
+
queue: message.queue,
|
|
257
|
+
name: message.name,
|
|
258
|
+
interface: message.interface,
|
|
259
|
+
status: message.status || "unknown",
|
|
260
|
+
paused: message.paused
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
@queues[message.queue] = q.with(members: q.members.merge(message.interface => updated_member))
|
|
264
|
+
rebuild_queues_table
|
|
265
|
+
return [self, wait_for_event_cmd]
|
|
266
|
+
|
|
267
|
+
when Messages::QueueMemberGone
|
|
268
|
+
if (q = @queues[message.queue])
|
|
269
|
+
@queues[message.queue] = q.with(members: q.members.reject { |k, _| k == message.interface })
|
|
270
|
+
rebuild_queues_table
|
|
271
|
+
end
|
|
272
|
+
return [self, wait_for_event_cmd]
|
|
273
|
+
|
|
274
|
+
when Messages::SystemInfo
|
|
275
|
+
@system_boot_at = message.received_at - message.uptime_secs
|
|
276
|
+
@last_reload_at = message.received_at - message.last_reload_secs
|
|
277
|
+
return [self, wait_for_event_cmd]
|
|
278
|
+
|
|
279
|
+
when Messages::AmiError
|
|
280
|
+
@error = message.error.to_s
|
|
281
|
+
return [self, wait_for_event_cmd]
|
|
282
|
+
|
|
283
|
+
when Messages::Tick
|
|
284
|
+
rebuild_table if @status == :connected && !@extensions.empty?
|
|
285
|
+
return [self, tick_cmd]
|
|
286
|
+
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
@spinner, spinner_cmd = @spinner.update(message)
|
|
290
|
+
[self, spinner_cmd]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def view
|
|
294
|
+
return Views::InfoModal.call(self) if @show_info
|
|
295
|
+
|
|
296
|
+
header = Views::Header.call(self)
|
|
297
|
+
body = build_body
|
|
298
|
+
footer = Views::Footer.call(self)
|
|
299
|
+
content = Lipgloss.join_vertical(:left, header, body, footer)
|
|
300
|
+
Lipgloss.place(@width, @height, :left, :top, content)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
private
|
|
304
|
+
|
|
305
|
+
def build_body
|
|
306
|
+
case @status
|
|
307
|
+
when :disconnected
|
|
308
|
+
Views::DisconnectedScreen.call(self)
|
|
309
|
+
when :connecting
|
|
310
|
+
""
|
|
311
|
+
else
|
|
312
|
+
if @view_mode == :calls
|
|
313
|
+
Views::ActiveCalls.render(@active_calls, @width, list_height(@active_calls.size), table: @calls_table)
|
|
314
|
+
elsif @view_mode == :queues
|
|
315
|
+
Views::QueueTable.render(@queues, @width, list_height(@queues.size), table: @queues_table)
|
|
316
|
+
else
|
|
317
|
+
@extensions.empty? ? Views::ExtensionTable.render_empty : (@table&.view || Views::ExtensionTable.render_empty)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def rebuild_table
|
|
323
|
+
table_height = [@height - 6, 5].max
|
|
324
|
+
@table = Views::ExtensionTable.build(@extensions, table_height)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def rebuild_calls_table
|
|
328
|
+
@calls_table = Views::ActiveCalls.build(@active_calls, list_height(@active_calls.size))
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def rebuild_queues_table
|
|
332
|
+
@queues_table = Views::QueueTable.build(@queues, list_height(@queues.size))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Rows available to a list body, never fewer than one. The upper bound is
|
|
336
|
+
# floored before clamping because a bare clamp(1, @height - 8) raises
|
|
337
|
+
# ArgumentError once the terminal is shorter than nine rows.
|
|
338
|
+
def list_height(count)
|
|
339
|
+
count.clamp(1, [@height - 8, 1].max)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def connect_cmd
|
|
343
|
+
-> {
|
|
344
|
+
begin
|
|
345
|
+
@bridge.connect_and_login
|
|
346
|
+
peers = @bridge.discover_peers
|
|
347
|
+
queues = @bridge.discover_queues
|
|
348
|
+
@bridge.subscribe
|
|
349
|
+
Messages::ConnectionEstablished.new(
|
|
350
|
+
remote: "#{@config.host}:#{@config.port}",
|
|
351
|
+
peers: peers,
|
|
352
|
+
queues: queues
|
|
353
|
+
)
|
|
354
|
+
rescue => e
|
|
355
|
+
Messages::ConnectionLost.new(reason: e.message)
|
|
356
|
+
end
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def wait_for_event_cmd
|
|
361
|
+
-> { @bridge.next_event }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def tick_cmd
|
|
365
|
+
Bubbletea.tick(TICK_INTERVAL) { Messages::Tick.new(at: Time.now) }
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def quit_key?(msg)
|
|
369
|
+
return true if msg.esc?
|
|
370
|
+
return true if msg.ctrl? && msg.key_type == Bubbletea::KeyMessage::KEY_CTRL_C
|
|
371
|
+
return true if msg.runes? && msg.char == "e"
|
|
372
|
+
|
|
373
|
+
false
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
data/lib/pbx/call.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pbx
|
|
4
|
+
CallQueue = Data.define(:name, :strategy, :calls_waiting, :completed, :abandoned, :holdtime, :last_holdtime, :members) do
|
|
5
|
+
def initialize(name:, strategy:, calls_waiting:, completed:, abandoned:, holdtime:, members:, last_holdtime: nil)
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
data/lib/pbx/cli.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "bubbletea"
|
|
5
|
+
require_relative "version"
|
|
6
|
+
require_relative "config"
|
|
7
|
+
require_relative "ami_bridge"
|
|
8
|
+
require_relative "app"
|
|
9
|
+
|
|
10
|
+
module Pbx
|
|
11
|
+
class CLI < Thor
|
|
12
|
+
desc "monitor", "Open the TUI and monitor Asterisk extension status"
|
|
13
|
+
long_desc <<~DESC
|
|
14
|
+
Connects to an Asterisk PBX via AMI, auto-discovers SIP/PJSIP extensions,
|
|
15
|
+
and displays real-time line status in a terminal UI.
|
|
16
|
+
|
|
17
|
+
Connection parameters can be supplied via CLI flags or a YAML config file
|
|
18
|
+
(use --config). CLI flags always override YAML values.
|
|
19
|
+
DESC
|
|
20
|
+
option :host, type: :string, desc: "AMI hostname or IP (default: 127.0.0.1)"
|
|
21
|
+
option :port, type: :numeric, desc: "AMI port (default: 5038)"
|
|
22
|
+
option :user, type: :string, desc: "AMI username"
|
|
23
|
+
option :secret, type: :string, desc: "AMI secret/password"
|
|
24
|
+
option :config, type: :string, aliases: "-c", desc: "Path to YAML config file"
|
|
25
|
+
option :debug, type: :boolean, default: false, desc: "Write AMI debug log to /tmp/pbx_debug.log"
|
|
26
|
+
def monitor
|
|
27
|
+
cli_opts = options.to_h.transform_keys(&:to_sym)
|
|
28
|
+
debug = cli_opts.delete(:debug) { false }
|
|
29
|
+
cfg = Config.load(cli: cli_opts)
|
|
30
|
+
bridge = AmiBridge.new(cfg, debug: debug)
|
|
31
|
+
Bubbletea.run(App.new(bridge: bridge, config: cfg), alt_screen: true)
|
|
32
|
+
rescue Config::Error => e
|
|
33
|
+
warn "Error: #{e.message}"
|
|
34
|
+
exit 1
|
|
35
|
+
ensure
|
|
36
|
+
bridge&.shutdown
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "version", "Print pbx version"
|
|
40
|
+
def version
|
|
41
|
+
puts "pbx #{Pbx::VERSION}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.exit_on_failure? = true
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/pbx/config.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Pbx
|
|
6
|
+
module Config
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
Value = Data.define(:host, :port, :user, :secret, :context, :reconnect_backoff) do
|
|
10
|
+
def complete?
|
|
11
|
+
user.to_s.strip.length.positive? && secret.to_s.strip.length.positive?
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
DEFAULTS = {
|
|
16
|
+
host: "127.0.0.1",
|
|
17
|
+
port: 5038,
|
|
18
|
+
user: nil,
|
|
19
|
+
secret: nil,
|
|
20
|
+
context: "default",
|
|
21
|
+
reconnect_backoff: [1, 2, 5, 10]
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def self.load(cli: {})
|
|
25
|
+
from_yaml = cli[:config] ? load_yaml(cli[:config]) : {}
|
|
26
|
+
merged = DEFAULTS.merge(from_yaml).merge(compact(cli).except(:config))
|
|
27
|
+
Value.new(**merged)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.load_yaml(path)
|
|
31
|
+
raw = YAML.safe_load_file(path, symbolize_names: true)
|
|
32
|
+
raw.slice(:host, :port, :user, :secret, :context, :reconnect_backoff)
|
|
33
|
+
rescue Errno::ENOENT
|
|
34
|
+
raise Error, "Config file not found: #{path}"
|
|
35
|
+
rescue Psych::Exception => e
|
|
36
|
+
raise Error, "Invalid YAML config: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.compact(hash)
|
|
40
|
+
hash.reject { |_, v| v.nil? }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_class_method :load_yaml, :compact
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/pbx/messages.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bubbletea"
|
|
4
|
+
|
|
5
|
+
module Pbx
|
|
6
|
+
module Messages
|
|
7
|
+
class PeerDiscovered < Bubbletea::Message
|
|
8
|
+
attr_reader :peer
|
|
9
|
+
|
|
10
|
+
def initialize(peer:)
|
|
11
|
+
super()
|
|
12
|
+
@peer = peer
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class PeerStatusChanged < Bubbletea::Message
|
|
17
|
+
attr_reader :peer_name, :status, :ip_address, :ip_port, :rtt_ms, :at
|
|
18
|
+
|
|
19
|
+
def initialize(peer_name:, status:, at:, ip_address: nil, ip_port: nil, rtt_ms: nil)
|
|
20
|
+
super()
|
|
21
|
+
@peer_name = peer_name
|
|
22
|
+
@status = status
|
|
23
|
+
@ip_address = ip_address
|
|
24
|
+
@ip_port = ip_port
|
|
25
|
+
@rtt_ms = rtt_ms
|
|
26
|
+
@at = at
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ConnectionEstablished < Bubbletea::Message
|
|
31
|
+
attr_reader :remote, :peers, :queues
|
|
32
|
+
|
|
33
|
+
def initialize(remote:, peers:, queues: {})
|
|
34
|
+
super()
|
|
35
|
+
@remote = remote
|
|
36
|
+
@peers = peers
|
|
37
|
+
@queues = queues
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class ConnectionLost < Bubbletea::Message
|
|
42
|
+
attr_reader :reason
|
|
43
|
+
|
|
44
|
+
def initialize(reason:)
|
|
45
|
+
super()
|
|
46
|
+
@reason = reason
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class AmiError < Bubbletea::Message
|
|
51
|
+
attr_reader :error
|
|
52
|
+
|
|
53
|
+
def initialize(error:)
|
|
54
|
+
super()
|
|
55
|
+
@error = error
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Tick < Bubbletea::Message
|
|
60
|
+
attr_reader :at
|
|
61
|
+
|
|
62
|
+
def initialize(at:)
|
|
63
|
+
super()
|
|
64
|
+
@at = at
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class CallStarted < Bubbletea::Message
|
|
69
|
+
attr_reader :uniqueid, :channel, :caller_id, :caller_name, :state, :started_at
|
|
70
|
+
|
|
71
|
+
def initialize(uniqueid:, channel:, caller_id:, caller_name:, state:, started_at:)
|
|
72
|
+
super()
|
|
73
|
+
@uniqueid = uniqueid
|
|
74
|
+
@channel = channel
|
|
75
|
+
@caller_id = caller_id
|
|
76
|
+
@caller_name = caller_name
|
|
77
|
+
@state = state
|
|
78
|
+
@started_at = started_at
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class CallEnded < Bubbletea::Message
|
|
83
|
+
attr_reader :uniqueid
|
|
84
|
+
|
|
85
|
+
def initialize(uniqueid:)
|
|
86
|
+
super()
|
|
87
|
+
@uniqueid = uniqueid
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class CallStateChanged < Bubbletea::Message
|
|
92
|
+
attr_reader :uniqueid, :state, :connected_to
|
|
93
|
+
|
|
94
|
+
def initialize(uniqueid:, state:, connected_to: nil)
|
|
95
|
+
super()
|
|
96
|
+
@uniqueid = uniqueid
|
|
97
|
+
@state = state
|
|
98
|
+
@connected_to = connected_to
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class DialCompleted < Bubbletea::Message
|
|
103
|
+
attr_reader :uniqueid, :dial_status
|
|
104
|
+
|
|
105
|
+
def initialize(uniqueid:, dial_status:)
|
|
106
|
+
super()
|
|
107
|
+
@uniqueid = uniqueid
|
|
108
|
+
@dial_status = dial_status
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class CallHeld < Bubbletea::Message
|
|
113
|
+
attr_reader :uniqueid
|
|
114
|
+
|
|
115
|
+
def initialize(uniqueid:)
|
|
116
|
+
super()
|
|
117
|
+
@uniqueid = uniqueid
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class CallUnheld < Bubbletea::Message
|
|
122
|
+
attr_reader :uniqueid
|
|
123
|
+
|
|
124
|
+
def initialize(uniqueid:)
|
|
125
|
+
super()
|
|
126
|
+
@uniqueid = uniqueid
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class CallDialplanUpdate < Bubbletea::Message
|
|
131
|
+
attr_reader :uniqueid, :context, :exten, :application
|
|
132
|
+
|
|
133
|
+
def initialize(uniqueid:, context:, exten:, application:)
|
|
134
|
+
super()
|
|
135
|
+
@uniqueid = uniqueid
|
|
136
|
+
@context = context
|
|
137
|
+
@exten = exten
|
|
138
|
+
@application = application
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class QueueCallCompleted < Bubbletea::Message
|
|
143
|
+
attr_reader :queue, :holdtime
|
|
144
|
+
|
|
145
|
+
def initialize(queue:, holdtime:)
|
|
146
|
+
super()
|
|
147
|
+
@queue = queue
|
|
148
|
+
@holdtime = holdtime
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class QueueCallerCountChanged < Bubbletea::Message
|
|
153
|
+
attr_reader :queue, :count
|
|
154
|
+
|
|
155
|
+
def initialize(queue:, count:)
|
|
156
|
+
super()
|
|
157
|
+
@queue = queue
|
|
158
|
+
@count = count
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
class QueueCallerAbandoned < Bubbletea::Message
|
|
163
|
+
attr_reader :queue
|
|
164
|
+
|
|
165
|
+
def initialize(queue:)
|
|
166
|
+
super()
|
|
167
|
+
@queue = queue
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
class QueueMemberUpdated < Bubbletea::Message
|
|
172
|
+
attr_reader :queue, :interface, :name, :status, :paused
|
|
173
|
+
|
|
174
|
+
def initialize(queue:, interface:, name:, status:, paused:)
|
|
175
|
+
super()
|
|
176
|
+
@queue = queue
|
|
177
|
+
@interface = interface
|
|
178
|
+
@name = name
|
|
179
|
+
@status = status
|
|
180
|
+
@paused = paused
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class QueueMemberGone < Bubbletea::Message
|
|
185
|
+
attr_reader :queue, :interface
|
|
186
|
+
|
|
187
|
+
def initialize(queue:, interface:)
|
|
188
|
+
super()
|
|
189
|
+
@queue = queue
|
|
190
|
+
@interface = interface
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
class SystemInfo < Bubbletea::Message
|
|
195
|
+
attr_reader :uptime_secs, :last_reload_secs, :received_at
|
|
196
|
+
|
|
197
|
+
def initialize(uptime_secs:, last_reload_secs:, received_at:)
|
|
198
|
+
super()
|
|
199
|
+
@uptime_secs = uptime_secs
|
|
200
|
+
@last_reload_secs = last_reload_secs
|
|
201
|
+
@received_at = received_at
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
data/lib/pbx/peer.rb
ADDED