yaic 0.1.0 → 0.2.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 +4 -4
- data/.claude/agents/ralph-qa.md +101 -0
- data/.claude/ralph/bin/dump-pid.sh +3 -0
- data/.claude/ralph/bin/kill-claude +6 -0
- data/.claude/ralph/bin/start-ralph +44 -0
- data/.claude/ralph/bin/stop-hook.sh +9 -0
- data/.claude/ralph/bin/stop-ralph +17 -0
- data/.claude/ralph/prompt.md +218 -0
- data/.claude/settings.json +26 -0
- data/.gitmodules +3 -0
- data/CLAUDE.md +65 -0
- data/README.md +106 -17
- data/Rakefile +8 -0
- data/devenv.nix +1 -0
- data/docs/agents/data-model.md +150 -0
- data/docs/agents/ralph/features/01-message-parsing.md.done +160 -0
- data/docs/agents/ralph/features/01-tcpsocket-refactor.md.done +109 -0
- data/docs/agents/ralph/features/02-connection-socket.md.done +138 -0
- data/docs/agents/ralph/features/02-simplified-client-api.md.done +306 -0
- data/docs/agents/ralph/features/03-registration.md.done +147 -0
- data/docs/agents/ralph/features/04-ping-pong.md.done +109 -0
- data/docs/agents/ralph/features/05-event-system.md.done +167 -0
- data/docs/agents/ralph/features/06-privmsg-notice.md.done +163 -0
- data/docs/agents/ralph/features/07-join-part.md.done +190 -0
- data/docs/agents/ralph/features/08-quit.md.done +118 -0
- data/docs/agents/ralph/features/09-nick-change.md.done +109 -0
- data/docs/agents/ralph/features/10-topic.md.done +145 -0
- data/docs/agents/ralph/features/11-kick.md.done +122 -0
- data/docs/agents/ralph/features/12-names.md.done +124 -0
- data/docs/agents/ralph/features/13-mode.md.done +174 -0
- data/docs/agents/ralph/features/14-who-whois.md.done +188 -0
- data/docs/agents/ralph/features/15-client-api.md.done +180 -0
- data/docs/agents/ralph/features/16-ssl-test-infrastructure.md.done +50 -0
- data/docs/agents/ralph/features/17-github-actions-ci.md.done +70 -0
- data/docs/agents/ralph/features/18-brakeman-security-scanning.md.done +67 -0
- data/docs/agents/ralph/features/19-fix-qa.md.done +73 -0
- data/docs/agents/ralph/features/20-test-optimization.md.done +70 -0
- data/docs/agents/ralph/features/21-test-parallelization.md.done +56 -0
- data/docs/agents/ralph/features/22-wait-until-pattern.md.done +90 -0
- data/docs/agents/ralph/features/23-ping-test-optimization.md.done +46 -0
- data/docs/agents/ralph/features/24-blocking-who-whois.md.done +159 -0
- data/docs/agents/ralph/features/25-verbose-mode.md.done +166 -0
- data/docs/agents/ralph/plans/test-optimization-plan.md +172 -0
- data/docs/agents/ralph/progress.md +731 -0
- data/docs/agents/todo.md +5 -0
- data/lib/yaic/channel.rb +22 -0
- data/lib/yaic/client.rb +821 -0
- data/lib/yaic/event.rb +35 -0
- data/lib/yaic/message.rb +119 -0
- data/lib/yaic/registration.rb +17 -0
- data/lib/yaic/socket.rb +120 -0
- data/lib/yaic/source.rb +39 -0
- data/lib/yaic/version.rb +1 -1
- data/lib/yaic/who_result.rb +17 -0
- data/lib/yaic/whois_result.rb +20 -0
- data/lib/yaic.rb +13 -1
- metadata +51 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Simplified Client API
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
The current Client API is broken. Users must:
|
|
6
|
+
- Call `client.on_socket_connected` manually (insane)
|
|
7
|
+
- Access internal socket via `instance_variable_get(:@socket)`
|
|
8
|
+
- Manually loop calling `socket.read`, `Message.parse`, `client.handle_message`
|
|
9
|
+
- Build custom `wait_for_*` helpers for every operation
|
|
10
|
+
|
|
11
|
+
The spec at `15-client-api.md.done` described the correct interface but the implementation didn't deliver. Fix it.
|
|
12
|
+
|
|
13
|
+
## Target API
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
client = Yaic::Client.new(
|
|
17
|
+
server: "irc.libera.chat",
|
|
18
|
+
port: 6697,
|
|
19
|
+
ssl: true,
|
|
20
|
+
nickname: "mynick"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
client.on(:message) { |event| puts event.text }
|
|
24
|
+
client.connect
|
|
25
|
+
|
|
26
|
+
client.join("#ruby")
|
|
27
|
+
client.privmsg("#ruby", "Hello!")
|
|
28
|
+
client.part("#ruby")
|
|
29
|
+
client.quit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That's it. All methods block until the operation completes. No bangs, no timeout params, no manual socket handling.
|
|
33
|
+
|
|
34
|
+
## Behavior
|
|
35
|
+
|
|
36
|
+
### All Operations Block
|
|
37
|
+
|
|
38
|
+
| Method | Blocks until |
|
|
39
|
+
|--------|--------------|
|
|
40
|
+
| `connect` | 001 RPL_WELCOME received |
|
|
41
|
+
| `join(channel)` | Channel appears in `@channels` |
|
|
42
|
+
| `part(channel)` | Channel removed from `@channels` |
|
|
43
|
+
| `nick(new)` | Nick change confirmed |
|
|
44
|
+
| `quit` | Disconnect complete |
|
|
45
|
+
|
|
46
|
+
### `connect` Method
|
|
47
|
+
|
|
48
|
+
Current (broken):
|
|
49
|
+
```ruby
|
|
50
|
+
client.connect
|
|
51
|
+
client.on_socket_connected # User must call this!
|
|
52
|
+
socket = client.instance_variable_get(:@socket) # Insane
|
|
53
|
+
# Manual loop...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
After:
|
|
57
|
+
```ruby
|
|
58
|
+
client.connect # Does everything, blocks until registered
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Internally `connect` must:
|
|
62
|
+
1. Create socket and connect (blocking with TCPSocket after feature 01)
|
|
63
|
+
2. Send NICK/USER registration
|
|
64
|
+
3. Start read loop (background thread)
|
|
65
|
+
4. Wait for 001 RPL_WELCOME
|
|
66
|
+
5. Set state to `:connected`
|
|
67
|
+
6. Return
|
|
68
|
+
|
|
69
|
+
### Read Loop
|
|
70
|
+
|
|
71
|
+
Background thread that runs continuously:
|
|
72
|
+
```ruby
|
|
73
|
+
loop do
|
|
74
|
+
raw = @socket.read
|
|
75
|
+
if raw
|
|
76
|
+
message = Message.parse(raw)
|
|
77
|
+
handle_message(message) if message
|
|
78
|
+
end
|
|
79
|
+
break if @state == :disconnected
|
|
80
|
+
sleep 0.001 # Prevent busy-wait when no data
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Internal Timeouts
|
|
85
|
+
|
|
86
|
+
Timeouts are an internal concern. Use sensible defaults:
|
|
87
|
+
- `connect`: 30 seconds
|
|
88
|
+
- `join`/`part`/`nick`: 10 seconds
|
|
89
|
+
|
|
90
|
+
Raise an error if exceeded. Users don't need to think about this.
|
|
91
|
+
|
|
92
|
+
### Thread Safety
|
|
93
|
+
|
|
94
|
+
- `@handlers` hash needs mutex protection for `on`/`off` during loop
|
|
95
|
+
- `@state` writes should be protected
|
|
96
|
+
- `@channels` needs mutex if accessed from handlers
|
|
97
|
+
|
|
98
|
+
### Remove Exposed Internals
|
|
99
|
+
|
|
100
|
+
- Remove `on_socket_connected` - internal use only
|
|
101
|
+
- `handle_message` can stay public for testing but document as internal
|
|
102
|
+
- Socket should never need to be accessed by users
|
|
103
|
+
|
|
104
|
+
## Tests
|
|
105
|
+
|
|
106
|
+
### Integration Tests - The Dream
|
|
107
|
+
|
|
108
|
+
After this feature, integration tests should look like:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
def test_two_clients_chat
|
|
112
|
+
client1 = Yaic::Client.new(server: "localhost", port: 6667, nickname: "alice")
|
|
113
|
+
client2 = Yaic::Client.new(server: "localhost", port: 6667, nickname: "bob")
|
|
114
|
+
|
|
115
|
+
received = []
|
|
116
|
+
client2.on(:message) { |e| received << e }
|
|
117
|
+
|
|
118
|
+
client1.connect
|
|
119
|
+
client2.connect
|
|
120
|
+
|
|
121
|
+
client1.join("#test")
|
|
122
|
+
client2.join("#test")
|
|
123
|
+
|
|
124
|
+
client1.privmsg("#test", "Hello Bob!")
|
|
125
|
+
sleep 0.5 # Let message arrive
|
|
126
|
+
|
|
127
|
+
assert_equal 1, received.size
|
|
128
|
+
assert_equal "Hello Bob!", received.first.text
|
|
129
|
+
ensure
|
|
130
|
+
client1&.quit
|
|
131
|
+
client2&.quit
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
No more:
|
|
136
|
+
- `instance_variable_get`
|
|
137
|
+
- Manual read loops
|
|
138
|
+
- Custom wait helpers
|
|
139
|
+
- `on_socket_connected`
|
|
140
|
+
|
|
141
|
+
### Unit Tests
|
|
142
|
+
|
|
143
|
+
**connect blocks until registered**
|
|
144
|
+
- Given: Mock socket that returns 001 after NICK/USER
|
|
145
|
+
- When: `client.connect`
|
|
146
|
+
- Then: Returns only after state is `:connected`
|
|
147
|
+
|
|
148
|
+
**connect handles nick collision**
|
|
149
|
+
- Given: Mock socket returns 433 then 001
|
|
150
|
+
- When: `client.connect`
|
|
151
|
+
- Then: Retries with underscore, eventually connects
|
|
152
|
+
|
|
153
|
+
**events fire from background thread**
|
|
154
|
+
- Given: Connected client with :message handler
|
|
155
|
+
- When: Server sends PRIVMSG
|
|
156
|
+
- Then: Handler called without user intervention
|
|
157
|
+
|
|
158
|
+
**quit stops the read loop**
|
|
159
|
+
- Given: Connected client
|
|
160
|
+
- When: `client.quit`
|
|
161
|
+
- Then: Background thread terminates cleanly
|
|
162
|
+
|
|
163
|
+
**on/off are thread-safe**
|
|
164
|
+
- Given: Read loop running
|
|
165
|
+
- When: Add handler from different thread
|
|
166
|
+
- Then: No race condition, handler works
|
|
167
|
+
|
|
168
|
+
**join blocks until confirmed**
|
|
169
|
+
- Given: Connected client
|
|
170
|
+
- When: `client.join("#test")`
|
|
171
|
+
- Then: Returns only after channel appears in `@channels`
|
|
172
|
+
|
|
173
|
+
**part blocks until confirmed**
|
|
174
|
+
- Given: Client in #test
|
|
175
|
+
- When: `client.part("#test")`
|
|
176
|
+
- Then: Returns only after channel removed from `@channels`
|
|
177
|
+
|
|
178
|
+
**nick blocks until confirmed**
|
|
179
|
+
- Given: Connected client
|
|
180
|
+
- When: `client.nick("newnick")`
|
|
181
|
+
- Then: Returns only after `@nick` updated
|
|
182
|
+
|
|
183
|
+
### Simplify Existing Integration Tests
|
|
184
|
+
|
|
185
|
+
All existing integration tests get dramatically simpler. Delete all the helper methods:
|
|
186
|
+
- `wait_for_connection`
|
|
187
|
+
- `wait_for_join`
|
|
188
|
+
- `wait_for_part`
|
|
189
|
+
|
|
190
|
+
Before:
|
|
191
|
+
```ruby
|
|
192
|
+
client.connect
|
|
193
|
+
client.on_socket_connected
|
|
194
|
+
socket = client.instance_variable_get(:@socket)
|
|
195
|
+
wait_for_connection(client, socket)
|
|
196
|
+
client.join(@test_channel)
|
|
197
|
+
wait_for_join(client, socket, @test_channel)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
After:
|
|
201
|
+
```ruby
|
|
202
|
+
client.connect
|
|
203
|
+
client.join(@test_channel)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Implementation Notes
|
|
207
|
+
|
|
208
|
+
### Structure
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
def connect
|
|
212
|
+
@socket = Socket.new(@server, @port, ssl: @ssl)
|
|
213
|
+
@socket.connect
|
|
214
|
+
send_registration
|
|
215
|
+
@state = :registering
|
|
216
|
+
start_read_loop
|
|
217
|
+
wait_until { @state == :connected }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def join(channel, key = nil)
|
|
221
|
+
params = key ? [channel, key] : [channel]
|
|
222
|
+
message = Message.new(command: "JOIN", params: params)
|
|
223
|
+
@socket.write(message.to_s)
|
|
224
|
+
wait_until { @channels.key?(channel) }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def part(channel, reason = nil)
|
|
228
|
+
params = reason ? [channel, reason] : [channel]
|
|
229
|
+
message = Message.new(command: "PART", params: params)
|
|
230
|
+
@socket.write(message.to_s)
|
|
231
|
+
wait_until { !@channels.key?(channel) }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def nick(new_nick)
|
|
235
|
+
message = Message.new(command: "NICK", params: [new_nick])
|
|
236
|
+
@socket.write(message.to_s)
|
|
237
|
+
wait_until { @nick == new_nick }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def wait_until(timeout: 10)
|
|
243
|
+
deadline = Time.now + timeout
|
|
244
|
+
until yield
|
|
245
|
+
raise "Operation timeout" if Time.now > deadline
|
|
246
|
+
sleep 0.01
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def start_read_loop
|
|
251
|
+
@read_thread = Thread.new do
|
|
252
|
+
loop do
|
|
253
|
+
break if @state == :disconnected
|
|
254
|
+
process_incoming
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def process_incoming
|
|
260
|
+
raw = @socket.read
|
|
261
|
+
return sleep(0.001) unless raw
|
|
262
|
+
|
|
263
|
+
message = Message.parse(raw)
|
|
264
|
+
handle_message(message) if message
|
|
265
|
+
rescue => e
|
|
266
|
+
emit(:error, nil, exception: e)
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Cleanup on Quit
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
def quit(reason = nil)
|
|
274
|
+
params = reason ? [reason] : []
|
|
275
|
+
message = Message.new(command: "QUIT", params: params)
|
|
276
|
+
@socket.write(message.to_s)
|
|
277
|
+
@channels.clear
|
|
278
|
+
@state = :disconnected
|
|
279
|
+
@read_thread&.join(5)
|
|
280
|
+
@socket&.disconnect
|
|
281
|
+
emit(:disconnect, nil)
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Graceful Error Handling
|
|
286
|
+
|
|
287
|
+
If socket dies unexpectedly:
|
|
288
|
+
- Set state to `:disconnected`
|
|
289
|
+
- Emit `:disconnect` event with error info
|
|
290
|
+
- Stop read loop
|
|
291
|
+
|
|
292
|
+
## Documentation
|
|
293
|
+
|
|
294
|
+
Create `README.md` with:
|
|
295
|
+
|
|
296
|
+
1. **Installation** - gem install / Gemfile
|
|
297
|
+
2. **Quick Start** - 10-line example that works
|
|
298
|
+
3. **Events** - List all events with their attributes
|
|
299
|
+
4. **Commands** - All IRC commands with examples
|
|
300
|
+
5. **Threading** - Brief note that a background thread handles reads
|
|
301
|
+
|
|
302
|
+
Keep it concise. The API should be obvious from the examples.
|
|
303
|
+
|
|
304
|
+
## Dependencies
|
|
305
|
+
|
|
306
|
+
- Requires `01-tcpsocket-refactor.md` (blocking connect simplifies this)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Connection Registration
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement the IRC connection registration sequence. After connecting, the client must send NICK and USER commands (optionally PASS) and wait for the server to acknowledge registration.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Registration Sequence
|
|
10
|
+
|
|
11
|
+
1. If password provided, send: `PASS <password>`
|
|
12
|
+
2. Send: `NICK <nickname>`
|
|
13
|
+
3. Send: `USER <username> 0 * :<realname>`
|
|
14
|
+
4. Wait for numeric 001 (RPL_WELCOME) to confirm registration
|
|
15
|
+
5. Transition state to `:connected`
|
|
16
|
+
|
|
17
|
+
### PASS Command
|
|
18
|
+
|
|
19
|
+
- Format: `PASS <password>`
|
|
20
|
+
- Must be sent before NICK/USER
|
|
21
|
+
- Only last PASS is used if sent multiple times
|
|
22
|
+
|
|
23
|
+
### NICK Command
|
|
24
|
+
|
|
25
|
+
- Format: `NICK <nickname>`
|
|
26
|
+
- Can be sent during or after registration
|
|
27
|
+
- Server may reject with:
|
|
28
|
+
- 431 ERR_NONICKNAMEGIVEN
|
|
29
|
+
- 432 ERR_ERRONEUSNICKNAME
|
|
30
|
+
- 433 ERR_NICKNAMEINUSE
|
|
31
|
+
|
|
32
|
+
### USER Command
|
|
33
|
+
|
|
34
|
+
- Format: `USER <username> 0 * :<realname>`
|
|
35
|
+
- The `0` and `*` are required (historical reasons)
|
|
36
|
+
- realname can contain spaces
|
|
37
|
+
|
|
38
|
+
### Handling Nick Collisions
|
|
39
|
+
|
|
40
|
+
If ERR_NICKNAMEINUSE (433) received during registration:
|
|
41
|
+
- Try alternative nick (append underscore or number)
|
|
42
|
+
- Emit event so user code can provide alternative
|
|
43
|
+
|
|
44
|
+
### Welcome Numerics
|
|
45
|
+
|
|
46
|
+
After successful registration, server sends:
|
|
47
|
+
- 001 RPL_WELCOME - Registration complete
|
|
48
|
+
- 002 RPL_YOURHOST - Server info
|
|
49
|
+
- 003 RPL_CREATED - Server creation time
|
|
50
|
+
- 004 RPL_MYINFO - Server name and supported modes
|
|
51
|
+
- 005 RPL_ISUPPORT - Server capabilities (multiple lines)
|
|
52
|
+
|
|
53
|
+
## Models
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
Yaic::Registration
|
|
57
|
+
- nickname: String
|
|
58
|
+
- username: String
|
|
59
|
+
- realname: String
|
|
60
|
+
- password: String or nil
|
|
61
|
+
- state: :pending, :nick_sent, :user_sent, :complete
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Tests
|
|
65
|
+
|
|
66
|
+
### Integration Tests - Successful Registration
|
|
67
|
+
|
|
68
|
+
**Register with nickname and user**
|
|
69
|
+
- Given: Connected to inspircd
|
|
70
|
+
- When: Send NICK and USER
|
|
71
|
+
- Then: Receive 001 RPL_WELCOME, state becomes :connected
|
|
72
|
+
|
|
73
|
+
**Register with password**
|
|
74
|
+
- Given: Connected to inspircd (configured with password)
|
|
75
|
+
- When: Send PASS, NICK, USER
|
|
76
|
+
- Then: Receive 001 RPL_WELCOME
|
|
77
|
+
|
|
78
|
+
### Integration Tests - Nick Handling
|
|
79
|
+
|
|
80
|
+
**Nick already in use**
|
|
81
|
+
- Given: Connected to inspircd, nick "taken" already connected
|
|
82
|
+
- When: Send NICK taken
|
|
83
|
+
- Then: Receive 433 ERR_NICKNAMEINUSE
|
|
84
|
+
|
|
85
|
+
**Invalid nickname**
|
|
86
|
+
- Given: Connected to inspircd
|
|
87
|
+
- When: Send NICK "#invalid"
|
|
88
|
+
- Then: Receive 432 ERR_ERRONEUSNICKNAME
|
|
89
|
+
|
|
90
|
+
**Empty nickname**
|
|
91
|
+
- Given: Connected to inspircd
|
|
92
|
+
- When: Send NICK with no parameter
|
|
93
|
+
- Then: Receive 431 ERR_NONICKNAMEGIVEN
|
|
94
|
+
|
|
95
|
+
### Unit Tests - Message Formatting
|
|
96
|
+
|
|
97
|
+
**Format PASS command**
|
|
98
|
+
- Given: password = "secret"
|
|
99
|
+
- When: Build PASS message
|
|
100
|
+
- Then: Output = "PASS secret\r\n"
|
|
101
|
+
|
|
102
|
+
**Format NICK command**
|
|
103
|
+
- Given: nickname = "mynick"
|
|
104
|
+
- When: Build NICK message
|
|
105
|
+
- Then: Output = "NICK mynick\r\n"
|
|
106
|
+
|
|
107
|
+
**Format USER command**
|
|
108
|
+
- Given: username = "myuser", realname = "My Real Name"
|
|
109
|
+
- When: Build USER message
|
|
110
|
+
- Then: Output = "USER myuser 0 * :My Real Name\r\n"
|
|
111
|
+
|
|
112
|
+
**Format USER with empty realname**
|
|
113
|
+
- Given: username = "myuser", realname = ""
|
|
114
|
+
- When: Build USER message
|
|
115
|
+
- Then: Output = "USER myuser 0 * :\r\n"
|
|
116
|
+
|
|
117
|
+
### State Machine Tests
|
|
118
|
+
|
|
119
|
+
**State transitions**
|
|
120
|
+
- Given: State = :disconnected
|
|
121
|
+
- When: Connect initiated
|
|
122
|
+
- Then: State = :connecting
|
|
123
|
+
|
|
124
|
+
- Given: State = :connecting
|
|
125
|
+
- When: Socket connected
|
|
126
|
+
- Then: State = :registering, NICK/USER sent
|
|
127
|
+
|
|
128
|
+
- Given: State = :registering
|
|
129
|
+
- When: 001 received
|
|
130
|
+
- Then: State = :connected
|
|
131
|
+
|
|
132
|
+
**State on nick collision**
|
|
133
|
+
- Given: State = :registering
|
|
134
|
+
- When: 433 received
|
|
135
|
+
- Then: State remains :registering, retry with alternate nick
|
|
136
|
+
|
|
137
|
+
## Implementation Notes
|
|
138
|
+
|
|
139
|
+
- Parse RPL_ISUPPORT (005) to learn server capabilities
|
|
140
|
+
- Store ISUPPORT values for later use (CHANTYPES, CHANMODES, etc.)
|
|
141
|
+
- Username may be prefixed with ~ if no ident server
|
|
142
|
+
- Some servers require PING response during registration
|
|
143
|
+
|
|
144
|
+
## Dependencies
|
|
145
|
+
|
|
146
|
+
- Requires `01-message-parsing.md`
|
|
147
|
+
- Requires `02-connection-socket.md`
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# PING/PONG Keepalive
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement PING/PONG handling to maintain connection with the server. This is critical - servers will disconnect clients that don't respond to PING.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Responding to PING
|
|
10
|
+
|
|
11
|
+
When server sends `PING <token>`:
|
|
12
|
+
1. Immediately respond with `PONG <token>`
|
|
13
|
+
2. Token must be identical to what server sent
|
|
14
|
+
3. Can occur at any time, including during registration
|
|
15
|
+
|
|
16
|
+
### Server PING Format
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
PING <token>
|
|
20
|
+
PING :<token>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The token may be with or without the trailing `:` prefix.
|
|
24
|
+
|
|
25
|
+
### Client PONG Response
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
PONG <token>
|
|
29
|
+
PONG :<token>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Mirror the token exactly. Do NOT include a server parameter.
|
|
33
|
+
|
|
34
|
+
### Connection Timeout Detection
|
|
35
|
+
|
|
36
|
+
If no data received from server for extended period (e.g., 180+ seconds):
|
|
37
|
+
- Connection may be dead
|
|
38
|
+
- Client should consider reconnecting
|
|
39
|
+
|
|
40
|
+
### Latency Measurement (Optional)
|
|
41
|
+
|
|
42
|
+
- Client may send `PING <token>` to server
|
|
43
|
+
- Server responds with `PONG <token>`
|
|
44
|
+
- Measure round-trip time
|
|
45
|
+
|
|
46
|
+
## Models
|
|
47
|
+
|
|
48
|
+
No new models required. Handled in message dispatch.
|
|
49
|
+
|
|
50
|
+
## Tests
|
|
51
|
+
|
|
52
|
+
### Integration Tests
|
|
53
|
+
|
|
54
|
+
**Respond to PING during registration**
|
|
55
|
+
- Given: Connected, registration in progress
|
|
56
|
+
- When: Server sends PING
|
|
57
|
+
- Then: Client responds with PONG, registration continues
|
|
58
|
+
|
|
59
|
+
**Respond to PING when connected**
|
|
60
|
+
- Given: Fully connected
|
|
61
|
+
- When: Server sends "PING :irc.example.com"
|
|
62
|
+
- Then: Client sends "PONG :irc.example.com"
|
|
63
|
+
|
|
64
|
+
**Handle PING without colon**
|
|
65
|
+
- Given: Fully connected
|
|
66
|
+
- When: Server sends "PING token123"
|
|
67
|
+
- Then: Client sends "PONG token123"
|
|
68
|
+
|
|
69
|
+
**Maintain connection over time**
|
|
70
|
+
- Given: Connected to inspircd
|
|
71
|
+
- When: Wait for server to send PING (typically 2-5 minutes)
|
|
72
|
+
- Then: Client responds, connection stays alive
|
|
73
|
+
|
|
74
|
+
### Unit Tests
|
|
75
|
+
|
|
76
|
+
**Parse PING message**
|
|
77
|
+
- Given: "PING :test.server.com\r\n"
|
|
78
|
+
- When: Parse message
|
|
79
|
+
- Then: command = "PING", params = ["test.server.com"]
|
|
80
|
+
|
|
81
|
+
**Build PONG response**
|
|
82
|
+
- Given: PING with token "abc123"
|
|
83
|
+
- When: Build PONG
|
|
84
|
+
- Then: Output = "PONG abc123\r\n"
|
|
85
|
+
|
|
86
|
+
**Build PONG with spaces in token**
|
|
87
|
+
- Given: PING with token "some server"
|
|
88
|
+
- When: Build PONG
|
|
89
|
+
- Then: Output = "PONG :some server\r\n"
|
|
90
|
+
|
|
91
|
+
### Timeout Tests
|
|
92
|
+
|
|
93
|
+
**Detect no data timeout**
|
|
94
|
+
- Given: Connected, no data received for 180 seconds
|
|
95
|
+
- When: Check connection health
|
|
96
|
+
- Then: Report connection as potentially dead
|
|
97
|
+
|
|
98
|
+
## Implementation Notes
|
|
99
|
+
|
|
100
|
+
- PING handling should be automatic, not requiring user code
|
|
101
|
+
- Process PING before other message handling to ensure quick response
|
|
102
|
+
- Log PING/PONG for debugging but don't emit as regular events
|
|
103
|
+
- Consider emitting a `:ping` event for latency tracking
|
|
104
|
+
|
|
105
|
+
## Dependencies
|
|
106
|
+
|
|
107
|
+
- Requires `01-message-parsing.md`
|
|
108
|
+
- Requires `02-connection-socket.md`
|
|
109
|
+
- Requires `03-registration.md` (PING can occur during registration)
|