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.
- checksums.yaml +5 -5
- data/.claude/skills/ruby-asterisk-agi/SKILL.md +317 -0
- data/.claude/skills/ruby-asterisk-ami/SKILL.md +187 -0
- data/.claude/skills/ruby-asterisk-ari/SKILL.md +174 -0
- data/.claude/skills/ruby-asterisk-ari-websocket/SKILL.md +148 -0
- data/.github/workflows/ci.yml +79 -0
- data/.gitignore +48 -4
- data/.rubocop.yml +84 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +3 -1
- data/README.md +410 -108
- data/Rakefile +3 -5
- data/lib/ruby-asterisk/agi/protocol.rb +160 -0
- data/lib/ruby-asterisk/agi/server.rb +108 -0
- data/lib/ruby-asterisk/agi/session.rb +187 -0
- data/lib/ruby-asterisk/ami/client.rb +135 -0
- data/lib/ruby-asterisk/ami/commands/channel.rb +55 -0
- data/lib/ruby-asterisk/ami/commands/conference.rb +37 -0
- data/lib/ruby-asterisk/ami/commands/extension.rb +17 -0
- data/lib/ruby-asterisk/ami/commands/mailbox.rb +21 -0
- data/lib/ruby-asterisk/ami/commands/monitor.rb +33 -0
- data/lib/ruby-asterisk/ami/commands/queue.rb +39 -0
- data/lib/ruby-asterisk/ami/commands/sip.rb +25 -0
- data/lib/ruby-asterisk/ami/commands/system.rb +50 -0
- data/lib/ruby-asterisk/ami/event.rb +22 -0
- data/lib/ruby-asterisk/ami/event_list_aggregation.rb +82 -0
- data/lib/ruby-asterisk/ami/parser.rb +60 -0
- data/lib/ruby-asterisk/ami/promise.rb +108 -0
- data/lib/ruby-asterisk/ami/reactor.rb +162 -0
- data/lib/ruby-asterisk/ari/client.rb +94 -0
- data/lib/ruby-asterisk/ari/resources/base.rb +23 -0
- data/lib/ruby-asterisk/ari/resources/bridge.rb +22 -0
- data/lib/ruby-asterisk/ari/resources/channel.rb +26 -0
- data/lib/ruby-asterisk/ari/resources/collection.rb +27 -0
- data/lib/ruby-asterisk/ari/resources/endpoint.rb +22 -0
- data/lib/ruby-asterisk/ari/resources/playback.rb +18 -0
- data/lib/ruby-asterisk/ari/websocket/connection.rb +143 -0
- data/lib/ruby-asterisk/ari/websocket/event_handlers.rb +80 -0
- data/lib/ruby-asterisk/ari/websocket/heartbeat.rb +65 -0
- data/lib/ruby-asterisk/ari/websocket/reconnect.rb +54 -0
- data/lib/ruby-asterisk/ari/websocket/socket_adapter.rb +23 -0
- data/lib/ruby-asterisk/ari/websocket.rb +155 -0
- data/lib/ruby-asterisk/compat.rb +7 -0
- data/lib/ruby-asterisk/error.rb +10 -0
- data/lib/ruby-asterisk/parsing_constants.rb +71 -69
- data/lib/ruby-asterisk/request.rb +33 -14
- data/lib/ruby-asterisk/response.rb +43 -16
- data/lib/ruby-asterisk/response_builder.rb +18 -0
- data/lib/ruby-asterisk/response_parser.rb +10 -9
- data/lib/ruby-asterisk/version.rb +4 -2
- data/lib/ruby-asterisk.rb +5 -222
- data/ruby-asterisk.gemspec +23 -17
- data/spec/ruby-asterisk/agi/protocol_spec.rb +239 -0
- data/spec/ruby-asterisk/agi/server_spec.rb +199 -0
- data/spec/ruby-asterisk/agi/session_spec.rb +293 -0
- data/spec/ruby-asterisk/ami/async_client_spec.rb +256 -0
- data/spec/ruby-asterisk/ami/multi_event_spec.rb +57 -0
- data/spec/ruby-asterisk/ami/parser_spec.rb +190 -0
- data/spec/ruby-asterisk/ami/reactor_spec.rb +290 -0
- data/spec/ruby-asterisk/ami_client_spec.rb +68 -0
- data/spec/ruby-asterisk/ari/client_spec.rb +168 -0
- data/spec/ruby-asterisk/ari/resources/bridge_spec.rb +42 -0
- data/spec/ruby-asterisk/ari/resources/channel_spec.rb +57 -0
- data/spec/ruby-asterisk/ari/resources/collection_spec.rb +66 -0
- data/spec/ruby-asterisk/ari/resources/endpoint_spec.rb +30 -0
- data/spec/ruby-asterisk/ari/resources/playback_spec.rb +33 -0
- data/spec/ruby-asterisk/ari/websocket_integration_spec.rb +86 -0
- data/spec/ruby-asterisk/ari/websocket_spec.rb +374 -0
- data/spec/ruby-asterisk/immutability_spec.rb +148 -0
- data/spec/ruby-asterisk/request_spec.rb +29 -9
- data/spec/ruby-asterisk/response_spec.rb +46 -47
- data/spec/spec_helper.rb +12 -0
- data/spec/support/mock_ami_server.rb +50 -0
- data/spec/support/mock_ari_websocket_server.rb +114 -0
- metadata +138 -18
- data/CHANGELOG +0 -3
- data/spec/ruby-asterisk/ruby_asterisk_spec.rb +0 -192
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ruby-asterisk-ari-websocket
|
|
3
|
+
description: Receive real-time ARI events from Asterisk via WebSocket with ruby-asterisk: StasisStart, ChannelStateChange, auto-reconnect, ping keep-alive, per-event callbacks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ruby-asterisk ARI WebSocket Client
|
|
7
|
+
|
|
8
|
+
## When to use this skill
|
|
9
|
+
|
|
10
|
+
Use this skill when you need to react to Asterisk events in real time — for example: starting call handling logic when a Stasis call arrives (`StasisStart`), updating a dashboard on channel state changes, or forwarding events to another system.
|
|
11
|
+
|
|
12
|
+
## Core concepts
|
|
13
|
+
|
|
14
|
+
- The WebSocket client runs a **connection thread that owns the socket read loop** (protocol handled by the pure Ruby `websocket-driver` gem — no EventMachine). `connect` returns immediately; event processing happens in that thread.
|
|
15
|
+
- Register handlers with `on(event_type, &block)` before or after connecting — handlers are stored and called whenever a matching event arrives.
|
|
16
|
+
- **Multiple handlers** for the same event type are supported; all are called in registration order.
|
|
17
|
+
- Auto-reconnect is enabled by default. When the connection drops, the client waits `reconnect_delay` seconds and reconnects. Set `auto_reconnect: false` to disable.
|
|
18
|
+
- A ping is sent every `ping_interval` seconds to keep the connection alive.
|
|
19
|
+
|
|
20
|
+
## Initialize
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require 'ruby-asterisk'
|
|
24
|
+
|
|
25
|
+
ws = RubyAsterisk::ARI::WebSocket.new(
|
|
26
|
+
'http://192.168.1.1:8088', # ARI base URL
|
|
27
|
+
'my_api_key', # API key (HTTP Basic auth username)
|
|
28
|
+
'my_stasis_app', # Stasis application name
|
|
29
|
+
auto_reconnect: true, # default: true
|
|
30
|
+
reconnect_delay: 5, # seconds between reconnect attempts; default: 5
|
|
31
|
+
ping_interval: 30, # keep-alive ping interval in seconds; default: 30
|
|
32
|
+
logger: Logger.new($stdout)
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API reference
|
|
37
|
+
|
|
38
|
+
| Method | Signature | Notes |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `on` | `(event_type, &block)` | Register a callback; `event_type` is a String or Symbol |
|
|
41
|
+
| `connect` | `(&block)` | Start the connection; optional block called on connect |
|
|
42
|
+
| `connected?` | — | `true` if socket is open and in OPEN state |
|
|
43
|
+
| `send_message?` | `(message)` | Send Hash (auto-JSON) or String; returns `false` if not connected |
|
|
44
|
+
| `disconnect` | — | Close the socket and disable auto-reconnect |
|
|
45
|
+
|
|
46
|
+
Constants (readable as class constants):
|
|
47
|
+
|
|
48
|
+
| Constant | Default |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `PING_INTERVAL` | 30 s |
|
|
51
|
+
| `RECONNECT_DELAY` | 5 s |
|
|
52
|
+
| `MAX_RECONNECT_ATTEMPTS` | `nil` (infinite) |
|
|
53
|
+
|
|
54
|
+
## Common ARI event types
|
|
55
|
+
|
|
56
|
+
| Event type | When fired |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `StasisStart` | A channel enters your Stasis application |
|
|
59
|
+
| `StasisEnd` | A channel leaves your Stasis application |
|
|
60
|
+
| `ChannelStateChange` | Channel state changes (ringing, up, etc.) |
|
|
61
|
+
| `ChannelDtmfReceived` | DTMF digit received on a channel |
|
|
62
|
+
| `ChannelHangupRequest` | Hangup requested on a channel |
|
|
63
|
+
| `BridgeCreated` / `BridgeDestroyed` | Bridge lifecycle |
|
|
64
|
+
| `ChannelEnteredBridge` / `ChannelLeftBridge` | Channel bridge membership |
|
|
65
|
+
| `PlaybackStarted` / `PlaybackFinished` | Media playback lifecycle |
|
|
66
|
+
|
|
67
|
+
Event payloads are plain Ruby Hashes (parsed from JSON). The outer `type` key matches the event type string.
|
|
68
|
+
|
|
69
|
+
## Usage patterns
|
|
70
|
+
|
|
71
|
+
### React to incoming Stasis calls
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
ws.on('StasisStart') do |event|
|
|
75
|
+
channel_id = event['channel']['id']
|
|
76
|
+
caller_id = event.dig('channel', 'caller', 'number')
|
|
77
|
+
puts "Incoming call from #{caller_id} on #{channel_id}"
|
|
78
|
+
|
|
79
|
+
# Use ARI HTTP client to control the channel
|
|
80
|
+
http = RubyAsterisk::ARI::Client.new('http://192.168.1.1:8088', 'key', 'my_stasis_app')
|
|
81
|
+
http.channels.get(channel_id).answer
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
ws.connect
|
|
85
|
+
sleep # keep main thread alive
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Handle disconnect and cleanup
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
ws.on('StasisEnd') do |event|
|
|
92
|
+
puts "Call ended: #{event['channel']['id']}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
ws.connect do
|
|
96
|
+
puts "Connected to ARI WebSocket"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Graceful shutdown
|
|
100
|
+
at_exit { ws.disconnect }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Conditional forwarding
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
ws.on('ChannelStateChange') do |event|
|
|
107
|
+
next unless event.dig('channel', 'state') == 'Up'
|
|
108
|
+
|
|
109
|
+
channel_id = event['channel']['id']
|
|
110
|
+
puts "Channel #{channel_id} is now answered"
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Combine with ARI HTTP client
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
http = RubyAsterisk::ARI::Client.new('http://192.168.1.1:8088', 'key', 'app')
|
|
118
|
+
|
|
119
|
+
ws = RubyAsterisk::ARI::WebSocket.new('http://192.168.1.1:8088', 'key', 'app')
|
|
120
|
+
ws.on('StasisStart') do |event|
|
|
121
|
+
channel = http.channels.get(event['channel']['id'])
|
|
122
|
+
channel.answer
|
|
123
|
+
channel.play('sound:hello-world')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
ws.connect
|
|
127
|
+
sleep
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Gotchas
|
|
131
|
+
|
|
132
|
+
- **`connect` is non-blocking** — it starts a background connection thread and returns `self`. The main thread must stay alive (e.g. `sleep`) for events to be processed.
|
|
133
|
+
- **Callbacks execute in the connection thread** — avoid long-running or blocking operations in callbacks; offload to a separate thread if needed. A blocked callback also blocks the read loop.
|
|
134
|
+
- **`send_message?` is thread-safe** — outgoing writes are serialized through an internal reentrant Monitor, so it can be called from any thread (including from inside a callback).
|
|
135
|
+
- **`send_message?` returns `false`** when not connected — always check the return value if delivery matters.
|
|
136
|
+
- **Auto-reconnect** is enabled by default. Call `disconnect` when you actually want to stop; just closing the socket will trigger a reconnect.
|
|
137
|
+
- **`app_name` must match** the Stasis application in Asterisk; events from other applications are not forwarded.
|
|
138
|
+
- Calling `on` after `connect` is safe — handlers are stored in a plain Hash and checked on each incoming message.
|
|
139
|
+
|
|
140
|
+
## Source files
|
|
141
|
+
|
|
142
|
+
- `lib/ruby-asterisk/ari/websocket.rb` — `WebSocket` class
|
|
143
|
+
- `lib/ruby-asterisk/ari/websocket/connection.rb` — `Connection` module (connect, read loop, reconnect)
|
|
144
|
+
- `lib/ruby-asterisk/ari/websocket/heartbeat.rb` — `Heartbeat` module (ping timer, interruptible waits)
|
|
145
|
+
- `lib/ruby-asterisk/ari/websocket/event_handlers.rb` — `EventHandlers` module (dispatch callbacks)
|
|
146
|
+
- `lib/ruby-asterisk/ari/websocket/socket_adapter.rb` — socket wrapper handed to `websocket-driver`
|
|
147
|
+
- `spec/ruby-asterisk/ari/websocket_spec.rb` — unit specs
|
|
148
|
+
- `spec/ruby-asterisk/ari/websocket_integration_spec.rb` — integration specs against `spec/support/mock_ari_websocket_server.rb`
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
# Required for the release job below: without a tags filter a tag push does
|
|
8
|
+
# not trigger this workflow at all, so `if: startsWith(github.ref,
|
|
9
|
+
# 'refs/tags/v')` would never be evaluated and no release could ever run.
|
|
10
|
+
tags:
|
|
11
|
+
- 'v*'
|
|
12
|
+
pull_request:
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lint:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
name: Linting
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- name: Set up Ruby
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: '3.4'
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: bundle install
|
|
27
|
+
- name: Run Rubocop
|
|
28
|
+
run: bundle exec rubocop
|
|
29
|
+
|
|
30
|
+
test:
|
|
31
|
+
needs: lint
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
strategy:
|
|
34
|
+
fail-fast: false
|
|
35
|
+
matrix:
|
|
36
|
+
ruby:
|
|
37
|
+
- '3.1'
|
|
38
|
+
- '3.2'
|
|
39
|
+
- '3.3'
|
|
40
|
+
- '3.4'
|
|
41
|
+
- '4.0.1'
|
|
42
|
+
- head
|
|
43
|
+
continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
|
|
44
|
+
name: Ruby ${{ matrix.ruby }}
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
- name: Set up Ruby
|
|
48
|
+
uses: ruby/setup-ruby@v1
|
|
49
|
+
with:
|
|
50
|
+
ruby-version: ${{ matrix.ruby }}
|
|
51
|
+
bundler-cache: true
|
|
52
|
+
- name: Install dependencies
|
|
53
|
+
run: bundle install
|
|
54
|
+
- name: Run tests
|
|
55
|
+
run: bundle exec rake spec
|
|
56
|
+
|
|
57
|
+
release:
|
|
58
|
+
needs: [lint, test]
|
|
59
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/checkout@v4
|
|
63
|
+
- name: Set up Ruby
|
|
64
|
+
uses: ruby/setup-ruby@v1
|
|
65
|
+
with:
|
|
66
|
+
ruby-version: '3.4'
|
|
67
|
+
- name: Install dependencies
|
|
68
|
+
run: bundle install
|
|
69
|
+
- name: Build gem
|
|
70
|
+
run: gem build *.gemspec
|
|
71
|
+
- name: Publish to RubyGems
|
|
72
|
+
run: |
|
|
73
|
+
mkdir -p $HOME/.gem
|
|
74
|
+
touch $HOME/.gem/credentials
|
|
75
|
+
chmod 0600 $HOME/.gem/credentials
|
|
76
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
77
|
+
gem push *.gem
|
|
78
|
+
env:
|
|
79
|
+
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
data/.gitignore
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
|
+
# Ignore bundler config.
|
|
2
|
+
/.bundle
|
|
3
|
+
|
|
4
|
+
# Ignore all logfiles and tempfiles.
|
|
5
|
+
/log/*
|
|
6
|
+
/tmp/*
|
|
7
|
+
/docs/*
|
|
8
|
+
!/log/.keep
|
|
9
|
+
!/tmp/.keep
|
|
10
|
+
|
|
11
|
+
# Ignore pidfiles, but keep the directory.
|
|
12
|
+
/tmp/pids/*
|
|
13
|
+
!/tmp/pids/
|
|
14
|
+
!/tmp/pids/.keep
|
|
15
|
+
|
|
16
|
+
# Ignore storage files.
|
|
17
|
+
/storage/*
|
|
18
|
+
!/storage/
|
|
19
|
+
!/storage/.keep
|
|
20
|
+
|
|
21
|
+
# Ignore master key for credentials.
|
|
22
|
+
/config/master.key
|
|
23
|
+
|
|
24
|
+
# Ignore locally-built DNS reverse lookup file.
|
|
25
|
+
/tmp/resolv.conf.pred
|
|
26
|
+
|
|
27
|
+
# Ignore local gems.
|
|
28
|
+
/vendor/bundle/
|
|
29
|
+
/vendor/gems/
|
|
30
|
+
/vendor/cache/
|
|
1
31
|
*.gem
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
32
|
+
/.gem/
|
|
33
|
+
|
|
34
|
+
# Ignore documentation files.
|
|
35
|
+
/doc/
|
|
36
|
+
/rdoc/
|
|
37
|
+
|
|
38
|
+
# Ignore compiled files.
|
|
39
|
+
*.o
|
|
40
|
+
*.so
|
|
41
|
+
|
|
42
|
+
# Ignore project-specific files
|
|
43
|
+
.idea/
|
|
44
|
+
.DS_Store
|
|
45
|
+
coverage/
|
|
6
46
|
.cane
|
|
7
47
|
*_high_water_mark
|
|
48
|
+
CLAUDE.md
|
|
49
|
+
GEMINI.md
|
|
50
|
+
pkg/
|
|
51
|
+
Gemfile.lock
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
plugins:
|
|
4
|
+
- rubocop-performance
|
|
5
|
+
|
|
6
|
+
AllCops:
|
|
7
|
+
TargetRubyVersion: 3.1
|
|
8
|
+
NewCops: enable
|
|
9
|
+
SuggestExtensions: false
|
|
10
|
+
|
|
11
|
+
# Crucial for Ractor compatibility - all files must have frozen string literal comment
|
|
12
|
+
Style/FrozenStringLiteralComment:
|
|
13
|
+
Enabled: true
|
|
14
|
+
EnforcedStyle: always
|
|
15
|
+
|
|
16
|
+
# Enforce single quotes unless interpolation is needed
|
|
17
|
+
Style/StringLiterals:
|
|
18
|
+
Enabled: true
|
|
19
|
+
EnforcedStyle: single_quotes
|
|
20
|
+
|
|
21
|
+
# Set reasonable limits but exclude large classes that group related commands
|
|
22
|
+
Metrics/ClassLength:
|
|
23
|
+
Enabled: true
|
|
24
|
+
Max: 100
|
|
25
|
+
Exclude:
|
|
26
|
+
- 'lib/ruby-asterisk/ami/client.rb'
|
|
27
|
+
- 'lib/ruby-asterisk/agi/session.rb'
|
|
28
|
+
|
|
29
|
+
# Reasonable method length
|
|
30
|
+
Metrics/MethodLength:
|
|
31
|
+
Enabled: true
|
|
32
|
+
Max: 15
|
|
33
|
+
Exclude:
|
|
34
|
+
- 'spec/**/*'
|
|
35
|
+
|
|
36
|
+
# Allow longer blocks in specs
|
|
37
|
+
Metrics/BlockLength:
|
|
38
|
+
Exclude:
|
|
39
|
+
- 'spec/**/*'
|
|
40
|
+
- '*.gemspec'
|
|
41
|
+
|
|
42
|
+
# Allow longer lines for readability in some cases
|
|
43
|
+
Layout/LineLength:
|
|
44
|
+
Max: 120
|
|
45
|
+
Exclude:
|
|
46
|
+
- 'spec/**/*'
|
|
47
|
+
|
|
48
|
+
# Gemspec file uses string literals from git commands
|
|
49
|
+
Gemspec/DevelopmentDependencies:
|
|
50
|
+
Enabled: false
|
|
51
|
+
|
|
52
|
+
# We explicitly use add_runtime_dependency for clarity about dependency types
|
|
53
|
+
Gemspec/AddRuntimeDependency:
|
|
54
|
+
Enabled: false
|
|
55
|
+
|
|
56
|
+
# Exclude legacy AMI class from parameter list checks until refactored
|
|
57
|
+
Metrics/ParameterLists:
|
|
58
|
+
Exclude:
|
|
59
|
+
- 'lib/ruby-asterisk.rb'
|
|
60
|
+
CountKeywordArgs: false
|
|
61
|
+
|
|
62
|
+
# Exclude legacy AMI class from optional boolean parameter checks
|
|
63
|
+
Style/OptionalBooleanParameter:
|
|
64
|
+
Exclude:
|
|
65
|
+
- 'lib/ruby-asterisk.rb'
|
|
66
|
+
|
|
67
|
+
# The gem filename follows rubygems convention (gem-name.rb)
|
|
68
|
+
Naming/FileName:
|
|
69
|
+
Exclude:
|
|
70
|
+
- 'lib/ruby-asterisk.rb'
|
|
71
|
+
|
|
72
|
+
# Exclude response parser complexity until refactored
|
|
73
|
+
Metrics/AbcSize:
|
|
74
|
+
Exclude:
|
|
75
|
+
- 'lib/ruby-asterisk/response_parser.rb'
|
|
76
|
+
|
|
77
|
+
# Keep `success` method name for backwards compatibility (not success?)
|
|
78
|
+
Naming/PredicateMethod:
|
|
79
|
+
Exclude:
|
|
80
|
+
- 'lib/ruby-asterisk/response.rb'
|
|
81
|
+
|
|
82
|
+
Lint/SuppressedException:
|
|
83
|
+
Exclude:
|
|
84
|
+
- 'spec/support/mock_ami_server.rb'
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-07-25
|
|
11
|
+
|
|
12
|
+
First release of the rewritten gem: three interfaces (AMI, ARI, AGI) on Ruby ≥ 3.1.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **AGI::Protocol** — stateless module for AGI wire protocol: `format_command`, `escape_argument`, `quote`, `parse_response`, `error?` (#55)
|
|
17
|
+
- **AGI::Protocol env helpers** — `parse_env_line`, `parse_env_block`, `collect_multiline_error` for robust Asterisk environment parsing and 520 multi-line error handling (#56)
|
|
18
|
+
- **AGI::Session** — full FastAGI session wrapper with fiber-friendly socket I/O: `answer`, `hangup`, `stream_file`, `say_digits`, `say_number`, `exec`, `set_variable`, `get_variable`, `get_data`, `verbose`, `dial`, `wait_for_digit`, `record_file`, `send_text`, `send_image`, `channel_status`, `database_get/put/del/deltree` (#55)
|
|
19
|
+
- **AGI::Server** — FastAGI TCP server backed by Async + Fiber scheduler; each connection runs in its own Fiber sharing a single OS thread (#54)
|
|
20
|
+
- **ARI::WebSocket** — WebSocket client for ARI real-time events: per-event callbacks via `on(type, &block)`, auto-reconnect, configurable ping keep-alive. Built on the pure Ruby `websocket-driver` gem over a plain `TCPSocket` (`SSLSocket` for https) — no EventMachine, so it installs and runs on every supported Ruby including 4.x: a connection thread owns connect/read-loop/reconnect, a ping thread sends keep-alives, and driver access is serialized through a reentrant Monitor so `send_message?` is thread-safe (#53, #82)
|
|
21
|
+
- **ARI::Client** — HTTP client for the Asterisk REST Interface backed by Faraday (#51)
|
|
22
|
+
- **ARI::Resources** — Channel, Bridge, Playback, Endpoint, Collection, and Base resource classes for the ARI HTTP client (#52)
|
|
23
|
+
- **AMI::Client async rewrite** — non-blocking AMI client with `Promise`-based responses: every command returns a `Promise`, call `.value(timeout)` to materialise the `Response`. The connection lives in a `Reactor` made of two plain OS threads (reader + writer) around the stateless `Parser` module, which gives deterministic shutdown on all Ruby ≥ 3.1 (#48, #49, #50, #79)
|
|
24
|
+
- **AMI command modules** — Channel, Conference, Extension, Mailbox, Monitor, Queue, Sip, System extracted into dedicated modules and mixed into `AMI::Client` (#47)
|
|
25
|
+
- `logoff` method on `AMI::Client`
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **BREAKING:** `RubyAsterisk::AMI::Client` replaces the legacy flat `RubyAsterisk::AMI` class; every command now returns `RubyAsterisk::AMI::Promise` instead of a synchronous `Response`.
|
|
30
|
+
- **BREAKING:** `Rami::VERSION` renamed to `RubyAsterisk::VERSION`; callers reading `Rami::VERSION` will get a `NameError`.
|
|
31
|
+
- Minimum Ruby version set to 3.1.
|
|
32
|
+
- Immutable domain objects (`Request`, `Response`) are now deep-frozen (#45).
|
|
33
|
+
- Namespace migration: all classes moved under `RubyAsterisk::` (#46).
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Ruby 3.1 compatibility: `IO#timeout` / `IO#timeout=` shim in `lib/ruby-asterisk/compat.rb`; the `async` gem's Fiber scheduler calls `io.timeout` in Ruby 3.2+ — returning `nil` on 3.1 disables that code path without breaking async I/O (#55).
|
|
38
|
+
- Deterministic teardown on `AMI::Client#disconnect`: both reactor threads are joined and every pending `Promise` is rejected instead of being left dangling (#50, #79).
|
|
39
|
+
- A caller-supplied `action_id:` (`status`, `extension_state`) is now used as the request's ActionID instead of being appended as a second `ActionID` header, which left the response uncorrelated with its `Promise`.
|
|
40
|
+
- `wait_event(timeout: -1)` no longer imposes a 5 s deadline on the returned `Promise`: a negative Timeout means Asterisk waits indefinitely, so `#value` now does too.
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
44
|
+
- `Net::Telnet` dependency (replaced by a raw `TCPSocket` owned by the AMI `Reactor`).
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## [0.1.0]
|
|
49
|
+
|
|
50
|
+
Legacy AMI client based on `Net::Telnet`. Supported: login, originate, hangup, queue operations, ConfBridge, extension state, SIP peers, device state, monitor/record, parked calls, skinny devices.
|
|
51
|
+
|
|
52
|
+
> Earlier history: see `git log v0.1.0` or the [full commit list on GitHub](https://github.com/emilianodellacasa/ruby-asterisk/commits/master).
|