ruby-asterisk 0.2.0 → 1.0.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.
Files changed (77) hide show
  1. checksums.yaml +5 -5
  2. data/.claude/skills/ruby-asterisk-agi/SKILL.md +317 -0
  3. data/.claude/skills/ruby-asterisk-ami/SKILL.md +187 -0
  4. data/.claude/skills/ruby-asterisk-ari/SKILL.md +174 -0
  5. data/.claude/skills/ruby-asterisk-ari-websocket/SKILL.md +148 -0
  6. data/.github/workflows/ci.yml +79 -0
  7. data/.gitignore +48 -4
  8. data/.rubocop.yml +84 -0
  9. data/CHANGELOG.md +52 -0
  10. data/Gemfile +3 -1
  11. data/README.md +410 -108
  12. data/Rakefile +3 -5
  13. data/lib/ruby-asterisk/agi/protocol.rb +160 -0
  14. data/lib/ruby-asterisk/agi/server.rb +108 -0
  15. data/lib/ruby-asterisk/agi/session.rb +187 -0
  16. data/lib/ruby-asterisk/ami/client.rb +135 -0
  17. data/lib/ruby-asterisk/ami/commands/channel.rb +55 -0
  18. data/lib/ruby-asterisk/ami/commands/conference.rb +37 -0
  19. data/lib/ruby-asterisk/ami/commands/extension.rb +17 -0
  20. data/lib/ruby-asterisk/ami/commands/mailbox.rb +21 -0
  21. data/lib/ruby-asterisk/ami/commands/monitor.rb +33 -0
  22. data/lib/ruby-asterisk/ami/commands/queue.rb +39 -0
  23. data/lib/ruby-asterisk/ami/commands/sip.rb +25 -0
  24. data/lib/ruby-asterisk/ami/commands/system.rb +50 -0
  25. data/lib/ruby-asterisk/ami/event.rb +22 -0
  26. data/lib/ruby-asterisk/ami/event_list_aggregation.rb +82 -0
  27. data/lib/ruby-asterisk/ami/parser.rb +60 -0
  28. data/lib/ruby-asterisk/ami/promise.rb +108 -0
  29. data/lib/ruby-asterisk/ami/reactor.rb +162 -0
  30. data/lib/ruby-asterisk/ari/client.rb +94 -0
  31. data/lib/ruby-asterisk/ari/resources/base.rb +23 -0
  32. data/lib/ruby-asterisk/ari/resources/bridge.rb +22 -0
  33. data/lib/ruby-asterisk/ari/resources/channel.rb +26 -0
  34. data/lib/ruby-asterisk/ari/resources/collection.rb +27 -0
  35. data/lib/ruby-asterisk/ari/resources/endpoint.rb +22 -0
  36. data/lib/ruby-asterisk/ari/resources/playback.rb +18 -0
  37. data/lib/ruby-asterisk/ari/websocket/connection.rb +143 -0
  38. data/lib/ruby-asterisk/ari/websocket/event_handlers.rb +80 -0
  39. data/lib/ruby-asterisk/ari/websocket/heartbeat.rb +65 -0
  40. data/lib/ruby-asterisk/ari/websocket/reconnect.rb +54 -0
  41. data/lib/ruby-asterisk/ari/websocket/socket_adapter.rb +23 -0
  42. data/lib/ruby-asterisk/ari/websocket.rb +155 -0
  43. data/lib/ruby-asterisk/compat.rb +7 -0
  44. data/lib/ruby-asterisk/error.rb +10 -0
  45. data/lib/ruby-asterisk/parsing_constants.rb +71 -69
  46. data/lib/ruby-asterisk/request.rb +33 -14
  47. data/lib/ruby-asterisk/response.rb +43 -16
  48. data/lib/ruby-asterisk/response_builder.rb +18 -0
  49. data/lib/ruby-asterisk/response_parser.rb +10 -9
  50. data/lib/ruby-asterisk/version.rb +4 -2
  51. data/lib/ruby-asterisk.rb +5 -222
  52. data/ruby-asterisk.gemspec +23 -17
  53. data/spec/ruby-asterisk/agi/protocol_spec.rb +239 -0
  54. data/spec/ruby-asterisk/agi/server_spec.rb +199 -0
  55. data/spec/ruby-asterisk/agi/session_spec.rb +293 -0
  56. data/spec/ruby-asterisk/ami/async_client_spec.rb +256 -0
  57. data/spec/ruby-asterisk/ami/multi_event_spec.rb +57 -0
  58. data/spec/ruby-asterisk/ami/parser_spec.rb +190 -0
  59. data/spec/ruby-asterisk/ami/reactor_spec.rb +290 -0
  60. data/spec/ruby-asterisk/ami_client_spec.rb +68 -0
  61. data/spec/ruby-asterisk/ari/client_spec.rb +168 -0
  62. data/spec/ruby-asterisk/ari/resources/bridge_spec.rb +42 -0
  63. data/spec/ruby-asterisk/ari/resources/channel_spec.rb +57 -0
  64. data/spec/ruby-asterisk/ari/resources/collection_spec.rb +66 -0
  65. data/spec/ruby-asterisk/ari/resources/endpoint_spec.rb +30 -0
  66. data/spec/ruby-asterisk/ari/resources/playback_spec.rb +33 -0
  67. data/spec/ruby-asterisk/ari/websocket_integration_spec.rb +86 -0
  68. data/spec/ruby-asterisk/ari/websocket_spec.rb +374 -0
  69. data/spec/ruby-asterisk/immutability_spec.rb +148 -0
  70. data/spec/ruby-asterisk/request_spec.rb +29 -9
  71. data/spec/ruby-asterisk/response_spec.rb +46 -47
  72. data/spec/spec_helper.rb +12 -0
  73. data/spec/support/mock_ami_server.rb +50 -0
  74. data/spec/support/mock_ari_websocket_server.rb +114 -0
  75. metadata +138 -18
  76. data/CHANGELOG +0 -3
  77. data/spec/ruby-asterisk/ruby_asterisk_spec.rb +0 -192
data/README.md CHANGED
@@ -1,248 +1,550 @@
1
- # RUBY-ASTERISK
1
+ # ruby-asterisk
2
2
 
3
- [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/emilianodellacasa/ruby-asterisk)
4
- [![Donate via Zerocracy](https://www.0crat.com/contrib-badge/CE8UGB6NP.svg)](https://www.0crat.com/contrib/CE8UGB6NP)
5
- [![Managed by Zerocracy](https://www.0crat.com/badge/CE8UGB6NP.svg)](https://www.0crat.com/p/CE8UGB6NP)
6
3
  [![Maintainability](https://api.codeclimate.com/v1/badges/2861728929db934eb376/maintainability)](https://codeclimate.com/github/emilianodellacasa/ruby-asterisk/maintainability)
7
4
 
8
- This gem add support to your Ruby or RubyOnRails projects to Asterisk Manager Interface
5
+ A Ruby client for [Asterisk PBX](https://www.asterisk.org/) covering three interfaces:
9
6
 
10
- There was a project with the same name, but it appears to be discontinued so I decided to start a new project
7
+ | Interface | Use case | Entry point | Concurrency model |
8
+ |---|---|---|---|
9
+ | **AMI** | TCP command/event channel | `RubyAsterisk::AMI::Client` | Threads + Promise (non-blocking) |
10
+ | **ARI HTTP** | REST control API | `RubyAsterisk::ARI::Client` | Synchronous (Faraday) |
11
+ | **ARI WebSocket** | Real-time event stream | `RubyAsterisk::ARI::WebSocket` | websocket-driver + Threads |
12
+ | **AGI / FastAGI** | In-process dialplan logic | `RubyAsterisk::AGI::Server` | Async + Fibers |
11
13
 
12
- ## Installation
14
+ ## Requirements
15
+
16
+ - **Ruby ≥ 3.1**
17
+ - Runtime dependencies: `async ~> 2.0`, `faraday >= 1.0`, `websocket-driver ~> 0.8`
13
18
 
14
- ### Rails3
19
+ > **Ruby 3.1 note**: the `IO#timeout` shim required by the Async Fiber scheduler is applied automatically when the AGI module is loaded — no action needed.
15
20
 
16
- Add to your Gemfile and run the `bundle` command to install it.
21
+ ## Installation
17
22
 
18
23
  ```ruby
19
- gem "ruby-asterisk"
24
+ gem 'ruby-asterisk'
25
+ ```
26
+
27
+ ```bash
28
+ bundle install
20
29
  ```
21
30
 
22
- ### Rails2
31
+ ## AMI — Asterisk Manager Interface
23
32
 
24
- Simply run in your terminal
33
+ ### Initialize and connect
25
34
 
26
35
  ```ruby
27
- gem install ruby-asterisk
28
- ```
36
+ require 'ruby-asterisk'
29
37
 
30
- ## Usage
38
+ ami = RubyAsterisk::AMI::Client.new(host: '192.168.1.1', port: 5038)
39
+ ami.connect
40
+ ```
31
41
 
32
- ### INITIALIZE
42
+ ### The Promise model
33
43
 
34
- To create a new AMI session, just call the following command
44
+ Every AMI command returns a `RubyAsterisk::AMI::Promise` immediately it does **not** block. Call `.value(timeout)` to wait for the `Response`:
35
45
 
36
46
  ```ruby
37
- @ami = RubyAsterisk::AMI.new("192.168.1.1",5038)
47
+ promise = ami.ping # returns immediately
48
+ response = promise.value # blocks up to 5 s (default)
49
+ response = promise.value(10) # custom timeout in seconds
50
+
51
+ puts response.success # => true
52
+ puts response.message # => "Pong"
53
+ puts response.action_id # => "lk3m0p..."
54
+ puts response.data # => structured Hash or Array depending on command
55
+ puts response.raw_response # => raw AMI text
56
+
57
+ # On timeout:
58
+ begin
59
+ response = promise.value(2)
60
+ rescue Timeout::Error => e
61
+ puts e.message
62
+ end
38
63
  ```
39
64
 
40
- ### LOGIN
65
+ `disconnect` rejects all pending promises with a `RuntimeError`.
41
66
 
42
- To log in, provide to the created sessions a valid username and password
67
+ ### Login / Logoff
43
68
 
44
69
  ```ruby
45
- @ami.login("mark","mysecret")
70
+ ami.login(username: 'mark', secret: 'mysecret').value
71
+ ami.logoff.value
46
72
  ```
47
73
 
48
- Like all commands, it will return a Response command that could be parsed accordingly
74
+ ### System commands
49
75
 
50
- ### CORE SHOW CHANNELS
76
+ ```ruby
77
+ ami.ping.value
78
+ ami.command('core show channels').value
79
+ ami.wait_event(timeout: -1).value # -1 = wait forever: .value blocks until an event arrives
80
+ ami.wait_event(timeout: 30).value # bounded wait; .value gives up after 30 s
81
+ ami.event_mask('on').value # enable event delivery on this connection
82
+ ami.parked_calls.value
83
+ ami.device_state_list.value
84
+ ami.skinny_devices.value
85
+ ami.skinny_lines.value
86
+ ```
51
87
 
52
- To get a list of all channels currently active on your Asterisk installation, use the following command
88
+ ### Channel commands
53
89
 
54
90
  ```ruby
55
- @ami.core_show_channels
91
+ ami.core_show_channels.value
92
+ ami.status.value # all channels
93
+ ami.status(channel: 'SIP/alice-001').value
94
+
95
+ # Originate: channel, context, callee, priority
96
+ ami.originate('SIP/9100', 'OUTGOING', '123456', '1',
97
+ caller_id: 'Alice',
98
+ variable: 'var1=12,var2=99',
99
+ timeout: 30_000,
100
+ async: true).value
101
+
102
+ # Originate to application
103
+ ami.originate_app(channel: 'SIP/9100',
104
+ application: 'Playback',
105
+ data: 'hello-world',
106
+ async: true).value
107
+
108
+ ami.redirect('SIP/alice-001', 'default', '100', '1').value
109
+ ami.hangup('SIP/alice-001').value
110
+ ami.atxfer(channel: 'SIP/alice-001',
111
+ exten: '200',
112
+ context: 'default',
113
+ priority: '1').value
56
114
  ```
57
115
 
58
- ### PARKED CALLS
59
-
60
- To get a list of all parked calls on your Asterisk PBX, use the following command
116
+ ### Queue commands
61
117
 
62
118
  ```ruby
63
- @ami.parked_calls
119
+ ami.queues.value
120
+ ami.queue_status.value
121
+ ami.queue_summary('support').value
122
+ ami.queue_add('support', 'SIP/agent1', penalty: 1, member_name: 'Alice').value
123
+ ami.queue_remove(queue: 'support', interface: 'SIP/agent1').value
124
+ ami.queue_pause(interface: 'SIP/agent1',
125
+ paused: true,
126
+ queue: 'support',
127
+ reason: 'lunch').value
64
128
  ```
65
129
 
66
- ### ORIGINATE
130
+ ### Conference commands
67
131
 
68
- To start a new call use the following command
132
+ ```ruby
133
+ ami.meet_me_list.value
134
+ ami.confbridges.value # list all ConfBridge rooms
135
+ ami.confbridge('my-room').value # list participants in a room
136
+ ami.confbridge_mute(conference: 'my-room', channel: 'SIP/alice-001').value
137
+ ami.confbridge_unmute(conference: 'my-room', channel: 'SIP/alice-001').value
138
+ ami.confbridge_kick(conference: 'my-room', channel: 'SIP/alice-001').value
139
+ ```
140
+
141
+ ### Mailbox commands
69
142
 
70
143
  ```ruby
71
- @ami.originate("SIP/9100","OUTGOING","123456","1","var1=12,var2=99") # CHANNEL, CONTEXT, CALLEE, PRIORITY, VARIABLES
144
+ ami.mailbox_status(mailbox: '1001', context: 'default').value
145
+ ami.mailbox_count(mailbox: '1001', context: 'default').value
72
146
  ```
73
147
 
148
+ ### SIP commands
74
149
 
75
- ### COMMAND
150
+ ```ruby
151
+ ami.sip_peers.value
152
+ ami.sip_show_peer('alice').value
153
+ ami.sip_show_registry.value
154
+ ```
76
155
 
77
- To execute a cli command use the following code
156
+ ### Extension state
78
157
 
79
158
  ```ruby
80
- @ami.command("core show channels")
159
+ response = ami.extension_state(exten: '1001', context: 'default').value
160
+ puts response.data[:hints]
161
+ # => [{ "Exten" => "1001", "Status" => "1",
162
+ # "DescriptiveStatus" => "In Use", ... }]
81
163
  ```
82
164
 
83
- ### MEETME LIST
165
+ Status values from `RubyAsterisk::DESCRIPTIVE_STATUS`:
166
+ `-1` Extension not found · `0` Idle · `1` In Use · `2` Busy · `3` Unavailable · `4` Ringing · `5` On Hold
167
+
168
+ `DescriptiveStatus` is only added for statuses in that map; any other value yields `nil`.
84
169
 
85
- To get a list of all active conferences use the following command
170
+ ### Monitor commands
86
171
 
87
172
  ```ruby
88
- @ami.meet_me_list
173
+ ami.monitor('SIP/alice-001', mix: true, file: '/tmp/call', format: 'wav').value
174
+ ami.pause_monitor('SIP/alice-001').value
175
+ ami.unpause_monitor('SIP/alice-001').value
176
+ ami.change_monitor(channel: 'SIP/alice-001', file: '/tmp/call2').value
177
+ ami.stop_monitor('SIP/alice-001').value
89
178
  ```
90
179
 
91
- ### EXTENSION STATE
92
-
93
- To get the state of an extension use the following command
180
+ ### Disconnect
94
181
 
95
182
  ```ruby
96
- @ami.extension_state(@exten,@context)
183
+ ami.disconnect
97
184
  ```
98
185
 
99
- ### DEVICE STATE LIST
186
+ ### The Response object
187
+
188
+ | Attribute | Type | Description |
189
+ |---|---|---|
190
+ | `success` | Boolean | `true` when raw response contains `Response: Success` |
191
+ | `action_id` | String | ActionID echoed by Asterisk |
192
+ | `message` | String | `Message:` field from the response |
193
+ | `data` | Hash / Array | Structured data parsed by `ResponseParser` (see `PARSE_DATA`) |
194
+ | `raw_response` | String/Array | The unprocessed AMI text |
195
+ | `type` | String | The AMI action name |
100
196
 
101
- To get list of states of devices
197
+ Commands with structured `data` parsing (`PARSE_DATA`): `CoreShowChannels`, `ParkedCalls`, `Originate`, `MeetMeList`, `ConfbridgeListRooms`, `ConfbridgeList`, `Status`, `ExtensionState`, `DeviceStateList`, `SKINNYdevices`, `SKINNYlines`, `QueuePause`, `Pong`, `Events`, `SIPpeers`, `SIPshowpeer`.
198
+
199
+ ---
200
+
201
+ ## ARI — Asterisk REST Interface
202
+
203
+ ### Initialize
102
204
 
103
205
  ```ruby
104
- @ami.device_state_list(@exten,@context)
105
- ```
206
+ require 'ruby-asterisk'
106
207
 
107
- ### SKINNY DEVICES AND LINES
208
+ client = RubyAsterisk::ARI::Client.new(
209
+ 'http://192.168.1.1:8088', # ARI base URL
210
+ 'my_api_key', # API key (username; password is empty)
211
+ 'my_stasis_app' # Stasis application name
212
+ )
213
+ ```
108
214
 
109
- To get list of skinny devices
215
+ ### Asterisk info
110
216
 
111
217
  ```ruby
112
- @ami.skinny_devices
218
+ info = client.asterisk_info
219
+ puts info['system']['version']
113
220
  ```
114
221
 
115
- To get list of skinny lines
222
+ ### Channels
116
223
 
117
224
  ```ruby
118
- @ami.skinny_lines
225
+ channel = client.channels.get('1553112062.1')
226
+ puts channel.id # => "1553112062.1"
227
+ puts channel.data['name'] # => "PJSIP/alice-00000001"
228
+
229
+ channels = client.channels.list
230
+ channels.each { |ch| puts ch.data['name'] }
231
+
232
+ channel.ring
233
+ channel.answer
234
+ channel.play('sound:hello-world')
235
+ channel.play('sound:tt-monkeys', playbackId: 'pb-1')
236
+ channel.hangup
119
237
  ```
120
238
 
121
- ### QUEUE PAUSE
122
-
123
- To pause or unpause a member in a call queue
124
-
239
+ ### Bridges
240
+
125
241
  ```ruby
126
- @ami.queue_pause("SIP/100", "true", "myqueue", "reason")
242
+ bridges = client.bridges.list
243
+ bridge = client.bridges.get('my-bridge-id')
244
+
245
+ bridge.add_channel('1553112062.1')
246
+ bridge.remove_channel('1553112062.1')
247
+ bridge.destroy
127
248
  ```
128
249
 
129
- ### PING
250
+ ### Playbacks
251
+
252
+ ```ruby
253
+ playback = client.playbacks.get('pb-1')
254
+ playback.control('pause')
255
+ playback.control('unpause')
256
+ playback.control('restart')
257
+ playback.stop
258
+ ```
130
259
 
131
- To ping asterisk AMI
260
+ ### Endpoints
132
261
 
133
262
  ```ruby
134
- @ami.ping
263
+ endpoints = client.endpoints.list
264
+ endpoints.each do |ep|
265
+ puts "#{ep.technology}/#{ep.resource} — #{ep.state}"
266
+ end
135
267
  ```
136
268
 
137
- ### EVENT MASK
269
+ ### Error handling
138
270
 
139
- To enable or disable sending events to this manager connection
271
+ All ARI methods raise `RubyAsterisk::Error` on HTTP 4xx/5xx:
140
272
 
141
273
  ```ruby
142
- @ami.event_mask("on")
274
+ begin
275
+ channel = client.channels.get('nonexistent-id')
276
+ rescue RubyAsterisk::Error => e
277
+ puts e.message # => "Channel not found"
278
+ end
143
279
  ```
144
280
 
145
- ### SIPpeers
281
+ ---
282
+
283
+ ## ARI WebSocket — Real-time events
146
284
 
147
- To get a list of sip peers (equivalent to "sip show peers" call on the asterisk server). This can be used to get a buddy list.
285
+ ### Initialize and connect
148
286
 
149
287
  ```ruby
150
- @ami.sip_peers
288
+ require 'ruby-asterisk'
289
+
290
+ ws = RubyAsterisk::ARI::WebSocket.new(
291
+ 'http://192.168.1.1:8088',
292
+ 'my_api_key',
293
+ 'my_stasis_app',
294
+ auto_reconnect: true, # default: true
295
+ reconnect_delay: 5, # seconds between attempts, default: 5
296
+ ping_interval: 30, # keep-alive ping interval, default: 30
297
+ logger: Logger.new($stdout)
298
+ )
151
299
  ```
152
300
 
153
- ### SIP SHOW PEER
301
+ ### Register event handlers
154
302
 
155
- To get info of a peer (equivalent to "sip show peer" call on the asterisk server).
303
+ ```ruby
304
+ ws.on('StasisStart') do |event|
305
+ puts "Call arrived on channel #{event['channel']['id']}"
306
+ end
307
+
308
+ ws.on('ChannelStateChange') do |event|
309
+ puts "Channel state: #{event['channel']['state']}"
310
+ end
311
+
312
+ ws.on('StasisEnd') do |event|
313
+ puts "Call ended: #{event['channel']['id']}"
314
+ end
315
+ ```
316
+
317
+ Multiple handlers for the same event type are supported.
318
+
319
+ ### Connect and disconnect
156
320
 
157
321
  ```ruby
158
- @ami.sip_show_peer(peer)
322
+ ws.connect do |sock|
323
+ puts "WebSocket connected"
324
+ end
325
+
326
+ # Check status
327
+ ws.connected? # => true / false
328
+
329
+ # Send a message (returns false if not connected)
330
+ ws.send_message?({ type: 'ping' })
331
+
332
+ # Disconnect (disables auto-reconnect)
333
+ ws.disconnect
334
+ ```
335
+
336
+ > **Threading note**: `connect` starts a background connection thread that owns the socket read loop. Event callbacks execute in that thread — avoid direct UI updates or non-thread-safe operations inside callbacks.
337
+
338
+ ---
339
+
340
+ ## AGI — FastAGI server
341
+
342
+ ### What is FastAGI?
343
+
344
+ FastAGI lets you run the AGI dialplan logic in an external process that Asterisk connects to via TCP. ruby-asterisk provides a server that handles each Asterisk connection in its own Fiber under the `async` gem, so many concurrent calls share a single OS thread.
345
+
346
+ In Asterisk's `extensions.conf`:
347
+
348
+ ```
349
+ exten => 100,1,AGI(agi://192.168.1.100:4573)
159
350
  ```
160
351
 
161
- ### STATUS
162
-
163
- To get a status of a single channel or for all channels
352
+ ### Initialize and start
353
+
164
354
  ```ruby
165
- @ami.status
355
+ require 'ruby-asterisk'
356
+
357
+ server = RubyAsterisk::AGI::Server.new('0.0.0.0', 4573, logger: Logger.new($stdout))
358
+ server.handle do |session|
359
+ session.answer
360
+ session.stream_file('hello-world')
361
+ session.hangup
362
+ end
363
+ server.run # blocks until server.stop is called
166
364
  ```
167
365
 
168
- ### ATXFER
366
+ The handler block **must** be registered with `handle` before calling `run`, otherwise `RubyAsterisk::Error` is raised.
367
+
368
+ ### Ephemeral port
369
+
370
+ Pass `port: 0` to bind to any free port; the actual bound port is available in `server.port` after `run` starts.
169
371
 
170
- Attendand transfer
372
+ ### Stop
171
373
 
172
374
  ```ruby
173
- @ami.atxfer(channel, exten, context, priority = '1')
375
+ # In a signal handler or another thread:
376
+ server.stop
174
377
  ```
175
378
 
176
- ### WAIT EVENT
379
+ Active sessions finish naturally after `stop` is called.
177
380
 
178
- Wait for an event to occur. Timeout in seconds to wait for events, -1 means forever
381
+ ### The Session object
382
+
383
+ Inside the handler block, `session` is a `RubyAsterisk::AGI::Session`. All command methods return a Hash: `{ code: Integer, result: String|nil, data: String|nil }`.
384
+
385
+ **Environment**
179
386
 
180
387
  ```ruby
181
- wait_event(timeout=-1)
388
+ session.env
389
+ # => { "agi_network" => "yes", "agi_channel" => "SIP/alice-001",
390
+ # "agi_callerid" => "0391234567", ... }
182
391
  ```
183
392
 
184
- ### MONITOR
393
+ **Call control**
185
394
 
186
- Monitor a channel
395
+ ```ruby
396
+ session.answer
397
+ session.hangup
398
+ session.hangup('SIP/alice-001') # explicit channel
399
+ session.channel_status # status of current channel
400
+ session.channel_status('SIP/b') # explicit channel
401
+ ```
402
+
403
+ **Audio playback**
187
404
 
188
405
  ```ruby
189
- monitor(channel,mix=false,file=nil,format='wav')
406
+ session.stream_file('hello-world') # play; no escape digits
407
+ session.stream_file('hello-world', '1234') # play; stop on digit 1/2/3/4
408
+ session.say_digits('12345')
409
+ session.say_digits('12345', '#') # stop on #
410
+ session.say_number(42)
411
+ session.say_number(42, '#*')
190
412
  ```
191
413
 
192
- ### STOP MONITOR
414
+ **DTMF and input**
193
415
 
194
- Stop monitoring a channel
416
+ ```ruby
417
+ result = session.get_data('enter-pin', timeout: 5000, max_digits: 4)
418
+ # Like every verb, returns the response Hash; the key's ASCII value
419
+ # (or -1 on timeout) is in response[:result].
420
+ response = session.wait_for_digit(5000)
421
+ digit = response[:result]
422
+ ```
423
+
424
+ **Variables**
195
425
 
196
426
  ```ruby
197
- stop_monitor(channel)
427
+ session.set_variable('MY_VAR', 'hello')
428
+ response = session.get_variable('MY_VAR')
429
+ puts response[:data] # => "hello"
198
430
  ```
199
431
 
200
- ### PAUSE MONITOR
432
+ **Recording**
201
433
 
202
- Pause monitoring of a channel
434
+ ```ruby
435
+ session.record_file('/tmp/greeting',
436
+ format: 'wav',
437
+ escape_digits: '#',
438
+ timeout_ms: 10_000,
439
+ beep: true,
440
+ silence: 3)
441
+ ```
442
+
443
+ **Execute dialplan application**
203
444
 
204
445
  ```ruby
205
- pause_monitor(channel)
446
+ session.exec('Playback', 'hello-world')
447
+ session.dial('PJSIP/bob', timeout: 30, options: 'r')
206
448
  ```
207
449
 
208
- ### UNPAUSE MONITOR
450
+ **AstDB**
209
451
 
210
- Unpause monitoring of a channel
452
+ ```ruby
453
+ session.database_put('myapp', 'last_caller', '0391234567')
454
+ response = session.database_get('myapp', 'last_caller')
455
+ puts response[:data] # => "0391234567"
456
+ session.database_del('myapp', 'last_caller')
457
+ session.database_deltree('myapp')
458
+ ```
459
+
460
+ **Logging and text**
211
461
 
212
462
  ```ruby
213
- unpause_monitor(channel)
463
+ session.verbose('Starting IVR flow', level: 2)
464
+ session.send_text('Hello from AGI')
465
+ session.send_image('logo.png')
214
466
  ```
215
467
 
216
- ### CHANGE MONITOR
468
+ ### IVR example — DTMF menu
469
+
470
+ ```ruby
471
+ server = RubyAsterisk::AGI::Server.new('0.0.0.0', 4573)
472
+ server.handle do |session|
473
+ session.answer
474
+
475
+ result = session.get_data('ivr-menu', timeout: 5000, max_digits: 1)
476
+ choice = result[:data].to_s.strip
477
+
478
+ case choice
479
+ when '1'
480
+ session.dial('PJSIP/sales', timeout: 30)
481
+ when '2'
482
+ session.dial('PJSIP/support', timeout: 30)
483
+ else
484
+ session.stream_file('invalid-option')
485
+ end
486
+
487
+ session.hangup
488
+ end
489
+ server.run
490
+ ```
217
491
 
218
- Change monitoring filename of a channel
492
+ ### Recording example with error handling
219
493
 
220
494
  ```ruby
221
- change_monitor(channel,file)
495
+ server = RubyAsterisk::AGI::Server.new('0.0.0.0', 4573)
496
+ server.handle do |session|
497
+ session.answer
498
+ session.stream_file('beep')
499
+
500
+ begin
501
+ session.record_file('/tmp/message', format: 'wav',
502
+ escape_digits: '#', timeout_ms: 30_000,
503
+ beep: false, silence: 5)
504
+ session.database_put('voicemail', 'last_recording', '/tmp/message')
505
+ rescue RubyAsterisk::Error => e
506
+ # e.message includes the 520 multi-line error body
507
+ session.verbose("Recording failed: #{e.message}", level: 1)
508
+ end
509
+
510
+ session.hangup
511
+ end
512
+ server.run
222
513
  ```
223
514
 
224
- ### THE RESPONSE OBJECT
515
+ ### Error handling in sessions
225
516
 
226
- The response object contains all information about all data received from Asterisk. Here follows a list of all object's properties:
517
+ `session.execute` (the underlying method) raises `RubyAsterisk::Error`:
518
+ - On EOF: `"Connection closed by Asterisk"`
519
+ - On AGI 5xx responses, including multi-line `520-…/520 End of proper usage.` errors
227
520
 
228
- - type
229
- - success
230
- - action_id
231
- - message
232
- - data
233
- - raw_response
521
+ All command methods (`answer`, `stream_file`, etc.) are thin wrappers around `execute` and propagate the same exception.
234
522
 
235
- The data property contains all additional information obtained from Asterisk, like for example the list of active channels after a "core show channels" command.
523
+ ---
236
524
 
237
- ## Todo List
525
+ ## LLM skill files
238
526
 
239
- - Adding initialization parameters for Telnet options like Output_log, Waittime, Dump_log, timeout;
240
- - Adding test cases for better code coverage;
241
- - Refactoring of ruby-asterisk.rb, adding method_missing for the purpose of supporting as much AMI commands as possible
527
+ Detailed how-to guides for each subsystem are in `.claude/skills/`. Claude Code loads them automatically as project-level skills; other agent runners can read them as markdown prompts.
528
+
529
+ | Skill file | Covers |
530
+ |---|---|
531
+ | `.claude/skills/ruby-asterisk-ami/SKILL.md` | AMI async client, Promise model, all command groups |
532
+ | `.claude/skills/ruby-asterisk-ari/SKILL.md` | ARI HTTP client, resources, error handling |
533
+ | `.claude/skills/ruby-asterisk-ari-websocket/SKILL.md` | ARI WebSocket events, callbacks, reconnect |
534
+ | `.claude/skills/ruby-asterisk-agi/SKILL.md` | FastAGI server, Session verbs, IVR and recording patterns |
535
+
536
+ ---
242
537
 
243
538
  ## Development
244
539
 
245
- Questions or problems? Please post them on the [issue tracker](https://github.com/emilianodellacasa/ruby-asterisk/issues). You can contribute changes by forking the project and submitting a pull request. You can ensure the tests passing by running `bundle` and `rake`.
540
+ ```bash
541
+ bundle install
542
+ bundle exec rake spec # run all tests
543
+ bundle exec rubocop # code quality checks
544
+ ```
545
+
546
+ Test infrastructure: `spec/support/mock_ami_server.rb` provides a mock AMI server for unit tests.
246
547
 
247
- This gem is created by Emiliano Della Casa and is under the MIT License and it is distributed by courtesy of [Engim srl](http://www.engim.eu/en).
548
+ Questions or issues? Please use the [issue tracker](https://github.com/emilianodellacasa/ruby-asterisk/issues). Contributions via fork + pull request are welcome.
248
549
 
550
+ This gem was created by Emiliano Della Casa and is licensed under the MIT License, distributed by courtesy of [Engim srl](http://www.engim.eu/en).
data/Rakefile CHANGED
@@ -1,12 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler'
2
4
  Bundler::GemHelper.install_tasks
3
5
 
4
6
  require 'rspec'
5
7
  require 'rspec/core/rake_task'
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require 'quality/rake/task'
9
-
10
- Quality::Rake::Task.new
8
+ RSpec::Core::RakeTask.new(:spec)
11
9
 
12
10
  task default: :spec