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,167 @@
|
|
|
1
|
+
# Event System
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement the event callback system that allows user code to react to IRC events. This is the primary interface for users of the library.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Registering Handlers
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client.on(:message) { |event| ... }
|
|
13
|
+
client.on(:join) { |event| ... }
|
|
14
|
+
client.on(:raw) { |message| ... }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Multiple handlers can be registered for the same event. They are called in order of registration.
|
|
18
|
+
|
|
19
|
+
### Event Dispatch
|
|
20
|
+
|
|
21
|
+
When an IRC message is received:
|
|
22
|
+
1. Parse the message
|
|
23
|
+
2. Determine event type(s) to emit
|
|
24
|
+
3. Build event payload
|
|
25
|
+
4. Call each registered handler with payload
|
|
26
|
+
5. Optionally emit `:raw` for every message
|
|
27
|
+
|
|
28
|
+
### Event Types
|
|
29
|
+
|
|
30
|
+
| Event | Trigger | Payload |
|
|
31
|
+
|-------|---------|---------|
|
|
32
|
+
| `:connect` | 001 RPL_WELCOME | `{server:}` |
|
|
33
|
+
| `:disconnect` | Connection closed | `{reason:}` |
|
|
34
|
+
| `:message` | PRIVMSG | `{source:, target:, text:}` |
|
|
35
|
+
| `:notice` | NOTICE | `{source:, target:, text:}` |
|
|
36
|
+
| `:join` | JOIN | `{channel:, user:}` |
|
|
37
|
+
| `:part` | PART | `{channel:, user:, reason:}` |
|
|
38
|
+
| `:quit` | QUIT | `{user:, reason:}` |
|
|
39
|
+
| `:kick` | KICK | `{channel:, user:, by:, reason:}` |
|
|
40
|
+
| `:nick` | NICK | `{old_nick:, new_nick:}` |
|
|
41
|
+
| `:topic` | TOPIC / 332 | `{channel:, topic:, setter:}` |
|
|
42
|
+
| `:mode` | MODE | `{target:, modes:, params:}` |
|
|
43
|
+
| `:error` | 4xx/5xx | `{numeric:, message:}` |
|
|
44
|
+
| `:raw` | Any message | `{message:}` |
|
|
45
|
+
|
|
46
|
+
### Handler Signature
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
client.on(:message) do |event|
|
|
50
|
+
event.source # Yaic::Source - who sent it
|
|
51
|
+
event.target # String - channel or nick
|
|
52
|
+
event.text # String - message content
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Error Handling in Handlers
|
|
57
|
+
|
|
58
|
+
If a handler raises an exception:
|
|
59
|
+
- Log the error
|
|
60
|
+
- Continue calling remaining handlers
|
|
61
|
+
- Do not crash the event loop
|
|
62
|
+
|
|
63
|
+
## Models
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
Yaic::Event
|
|
67
|
+
- type: Symbol
|
|
68
|
+
- attributes: Hash
|
|
69
|
+
- message: Yaic::Message (original parsed message)
|
|
70
|
+
- method_missing for attribute access
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Tests
|
|
74
|
+
|
|
75
|
+
### Unit Tests - Handler Registration
|
|
76
|
+
|
|
77
|
+
**Register single handler**
|
|
78
|
+
- Given: New client
|
|
79
|
+
- When: Register handler for :message
|
|
80
|
+
- Then: Handler is stored
|
|
81
|
+
|
|
82
|
+
**Register multiple handlers for same event**
|
|
83
|
+
- Given: New client
|
|
84
|
+
- When: Register 3 handlers for :message
|
|
85
|
+
- Then: All 3 handlers stored in order
|
|
86
|
+
|
|
87
|
+
**Register handlers for different events**
|
|
88
|
+
- Given: New client
|
|
89
|
+
- When: Register handlers for :message, :join, :part
|
|
90
|
+
- Then: Each event has its handlers
|
|
91
|
+
|
|
92
|
+
### Unit Tests - Event Dispatch
|
|
93
|
+
|
|
94
|
+
**Dispatch calls all handlers**
|
|
95
|
+
- Given: 3 handlers registered for :message
|
|
96
|
+
- When: Dispatch :message event
|
|
97
|
+
- Then: All 3 handlers called in order
|
|
98
|
+
|
|
99
|
+
**Dispatch with correct payload**
|
|
100
|
+
- Given: Handler registered for :message
|
|
101
|
+
- When: PRIVMSG received from "nick!user@host" to "#chan" with text "hello"
|
|
102
|
+
- Then: Handler receives event with source.nick="nick", target="#chan", text="hello"
|
|
103
|
+
|
|
104
|
+
**Handler exception doesn't stop others**
|
|
105
|
+
- Given: 3 handlers, second one raises
|
|
106
|
+
- When: Dispatch event
|
|
107
|
+
- Then: First and third handlers still called
|
|
108
|
+
|
|
109
|
+
**Unknown event type**
|
|
110
|
+
- Given: No handlers registered for :foo
|
|
111
|
+
- When: Dispatch :foo event
|
|
112
|
+
- Then: No error, silently ignored
|
|
113
|
+
|
|
114
|
+
### Unit Tests - Event Type Detection
|
|
115
|
+
|
|
116
|
+
**PRIVMSG triggers :message**
|
|
117
|
+
- Given: PRIVMSG message received
|
|
118
|
+
- When: Determine event type
|
|
119
|
+
- Then: Returns :message
|
|
120
|
+
|
|
121
|
+
**NOTICE triggers :notice**
|
|
122
|
+
- Given: NOTICE message received
|
|
123
|
+
- When: Determine event type
|
|
124
|
+
- Then: Returns :notice
|
|
125
|
+
|
|
126
|
+
**JOIN triggers :join**
|
|
127
|
+
- Given: JOIN message received
|
|
128
|
+
- When: Determine event type
|
|
129
|
+
- Then: Returns :join
|
|
130
|
+
|
|
131
|
+
**001 triggers :connect**
|
|
132
|
+
- Given: 001 numeric received
|
|
133
|
+
- When: Determine event type
|
|
134
|
+
- Then: Returns :connect
|
|
135
|
+
|
|
136
|
+
**4xx/5xx triggers :error**
|
|
137
|
+
- Given: 433 numeric received
|
|
138
|
+
- When: Determine event type
|
|
139
|
+
- Then: Returns :error with numeric in payload
|
|
140
|
+
|
|
141
|
+
### Integration Tests
|
|
142
|
+
|
|
143
|
+
**End-to-end message event**
|
|
144
|
+
- Given: Connected client with :message handler
|
|
145
|
+
- When: Another client sends PRIVMSG
|
|
146
|
+
- Then: Handler called with correct source, target, text
|
|
147
|
+
|
|
148
|
+
**End-to-end join event**
|
|
149
|
+
- Given: Connected client with :join handler
|
|
150
|
+
- When: Client joins #test
|
|
151
|
+
- Then: Handler called with channel="#test", user=self
|
|
152
|
+
|
|
153
|
+
**Multiple events from one message**
|
|
154
|
+
- Given: Connected client with :raw and :message handlers
|
|
155
|
+
- When: PRIVMSG received
|
|
156
|
+
- Then: Both :raw and :message handlers called
|
|
157
|
+
|
|
158
|
+
## Implementation Notes
|
|
159
|
+
|
|
160
|
+
- Use simple array of [event_type, block] pairs
|
|
161
|
+
- Consider using a Queue for thread-safe dispatch
|
|
162
|
+
- Events should be fired synchronously (blocking until all handlers complete)
|
|
163
|
+
- Provide `client.off(:event)` to remove handlers if needed
|
|
164
|
+
|
|
165
|
+
## Dependencies
|
|
166
|
+
|
|
167
|
+
- Requires `01-message-parsing.md` (to parse incoming messages)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# PRIVMSG and NOTICE
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement sending and receiving private messages and notices. This is the core messaging functionality of IRC.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Sending PRIVMSG
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client.privmsg("#channel", "Hello everyone!")
|
|
13
|
+
client.privmsg("nickname", "Hello you!")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Format: `PRIVMSG <target> :<text>`
|
|
17
|
+
|
|
18
|
+
Target can be:
|
|
19
|
+
- Channel name (e.g., `#ruby`)
|
|
20
|
+
- Nickname (e.g., `dan`)
|
|
21
|
+
|
|
22
|
+
### Sending NOTICE
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
client.notice("#channel", "Announcement!")
|
|
26
|
+
client.notice("nickname", "FYI...")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Format: `NOTICE <target> :<text>`
|
|
30
|
+
|
|
31
|
+
Same targets as PRIVMSG. Key difference: bots should use NOTICE to avoid loops.
|
|
32
|
+
|
|
33
|
+
### Receiving PRIVMSG
|
|
34
|
+
|
|
35
|
+
Incoming format: `:nick!user@host PRIVMSG <target> :<text>`
|
|
36
|
+
|
|
37
|
+
Parse and emit `:message` event with:
|
|
38
|
+
- `source` - Yaic::Source of sender
|
|
39
|
+
- `target` - Channel or own nick
|
|
40
|
+
- `text` - Message content
|
|
41
|
+
|
|
42
|
+
### Receiving NOTICE
|
|
43
|
+
|
|
44
|
+
Incoming format: `:nick!user@host NOTICE <target> :<text>`
|
|
45
|
+
|
|
46
|
+
Parse and emit `:notice` event with same fields as :message.
|
|
47
|
+
|
|
48
|
+
### Error Responses
|
|
49
|
+
|
|
50
|
+
- 401 ERR_NOSUCHNICK - Target user doesn't exist
|
|
51
|
+
- 404 ERR_CANNOTSENDTOCHAN - Cannot send to channel (banned, moderated, not joined)
|
|
52
|
+
- 407 ERR_TOOMANYTARGETS - Too many targets
|
|
53
|
+
|
|
54
|
+
### Message Length
|
|
55
|
+
|
|
56
|
+
- Messages over 512 bytes (total) will be truncated by server
|
|
57
|
+
- Client should warn or split long messages
|
|
58
|
+
- Usable text length ≈ 400-450 chars depending on nick/channel length
|
|
59
|
+
|
|
60
|
+
## Models
|
|
61
|
+
|
|
62
|
+
No new models. Uses existing Message and Event.
|
|
63
|
+
|
|
64
|
+
## Tests
|
|
65
|
+
|
|
66
|
+
### Integration Tests - Sending
|
|
67
|
+
|
|
68
|
+
**Send PRIVMSG to channel**
|
|
69
|
+
- Given: Client joined to #test
|
|
70
|
+
- When: `client.privmsg("#test", "Hello")`
|
|
71
|
+
- Then: Server receives "PRIVMSG #test :Hello"
|
|
72
|
+
|
|
73
|
+
**Send PRIVMSG to user**
|
|
74
|
+
- Given: Connected client
|
|
75
|
+
- When: `client.privmsg("othernick", "Hello")`
|
|
76
|
+
- Then: Server receives "PRIVMSG othernick :Hello"
|
|
77
|
+
|
|
78
|
+
**Send NOTICE to channel**
|
|
79
|
+
- Given: Client joined to #test
|
|
80
|
+
- When: `client.notice("#test", "Announcement")`
|
|
81
|
+
- Then: Server receives "NOTICE #test :Announcement"
|
|
82
|
+
|
|
83
|
+
**Send message with special characters**
|
|
84
|
+
- Given: Connected client
|
|
85
|
+
- When: `client.privmsg("#test", "Hello :) world")`
|
|
86
|
+
- Then: Server receives "PRIVMSG #test :Hello :) world" (colon preserved)
|
|
87
|
+
|
|
88
|
+
### Integration Tests - Receiving
|
|
89
|
+
|
|
90
|
+
**Receive PRIVMSG from user**
|
|
91
|
+
- Given: Client with :message handler
|
|
92
|
+
- When: Other user sends PRIVMSG to client's nick
|
|
93
|
+
- Then: Handler receives event with source, target=own_nick, text
|
|
94
|
+
|
|
95
|
+
**Receive PRIVMSG in channel**
|
|
96
|
+
- Given: Client joined #test with :message handler
|
|
97
|
+
- When: Other user sends PRIVMSG to #test
|
|
98
|
+
- Then: Handler receives event with source, target="#test", text
|
|
99
|
+
|
|
100
|
+
**Receive NOTICE**
|
|
101
|
+
- Given: Client with :notice handler
|
|
102
|
+
- When: Server sends NOTICE
|
|
103
|
+
- Then: Handler receives event with source, target, text
|
|
104
|
+
|
|
105
|
+
**Distinguish channel from private message**
|
|
106
|
+
- Given: :message handler that tracks target
|
|
107
|
+
- When: Receive PRIVMSG to "#chan" vs PRIVMSG to "mynick"
|
|
108
|
+
- Then: target reflects correct destination
|
|
109
|
+
|
|
110
|
+
### Unit Tests - Message Formatting
|
|
111
|
+
|
|
112
|
+
**Format PRIVMSG**
|
|
113
|
+
- Given: target = "#test", text = "Hello"
|
|
114
|
+
- When: Build PRIVMSG
|
|
115
|
+
- Then: Output = "PRIVMSG #test :Hello\r\n"
|
|
116
|
+
|
|
117
|
+
**Format PRIVMSG with colon in text**
|
|
118
|
+
- Given: target = "#test", text = ":smile:"
|
|
119
|
+
- When: Build PRIVMSG
|
|
120
|
+
- Then: Output = "PRIVMSG #test ::smile:\r\n"
|
|
121
|
+
|
|
122
|
+
**Format NOTICE**
|
|
123
|
+
- Given: target = "nick", text = "Info"
|
|
124
|
+
- When: Build NOTICE
|
|
125
|
+
- Then: Output = "NOTICE nick :Info\r\n"
|
|
126
|
+
|
|
127
|
+
### Unit Tests - Event Parsing
|
|
128
|
+
|
|
129
|
+
**Parse PRIVMSG event**
|
|
130
|
+
- Given: `:dan!d@host PRIVMSG #ruby :Hello everyone`
|
|
131
|
+
- When: Parse and create event
|
|
132
|
+
- Then: event.type = :message, source.nick = "dan", target = "#ruby", text = "Hello everyone"
|
|
133
|
+
|
|
134
|
+
**Parse NOTICE event**
|
|
135
|
+
- Given: `:server NOTICE * :Looking up hostname`
|
|
136
|
+
- When: Parse and create event
|
|
137
|
+
- Then: event.type = :notice, source = server, target = "*", text = "Looking up hostname"
|
|
138
|
+
|
|
139
|
+
### Error Handling Tests
|
|
140
|
+
|
|
141
|
+
**Send to non-existent nick**
|
|
142
|
+
- Given: Connected client
|
|
143
|
+
- When: `client.privmsg("nonexistent", "Hello")`
|
|
144
|
+
- Then: Receive 401 ERR_NOSUCHNICK, :error event emitted
|
|
145
|
+
|
|
146
|
+
**Send to channel not joined**
|
|
147
|
+
- Given: Connected client, not in #secret
|
|
148
|
+
- When: `client.privmsg("#secret", "Hello")`
|
|
149
|
+
- Then: Receive 404 ERR_CANNOTSENDTOCHAN
|
|
150
|
+
|
|
151
|
+
## Implementation Notes
|
|
152
|
+
|
|
153
|
+
- PRIVMSG text always needs trailing colon prefix (may contain spaces)
|
|
154
|
+
- Consider convenience method: `client.msg(target, text)` as alias for privmsg
|
|
155
|
+
- Bot authors should use notice() for automatic responses
|
|
156
|
+
- Track own nick to detect private vs channel messages
|
|
157
|
+
|
|
158
|
+
## Dependencies
|
|
159
|
+
|
|
160
|
+
- Requires `01-message-parsing.md`
|
|
161
|
+
- Requires `02-connection-socket.md`
|
|
162
|
+
- Requires `03-registration.md` (must be registered to send)
|
|
163
|
+
- Requires `05-event-system.md`
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# JOIN and PART
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement joining and leaving IRC channels. This is essential for participating in channel conversations.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### JOIN Command
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client.join("#ruby")
|
|
13
|
+
client.join("#ruby", "channelkey") # For key-protected channels
|
|
14
|
+
client.join("#a,#b,#c") # Multiple channels
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Format: `JOIN <channel>[,<channel>...] [<key>[,<key>...]]`
|
|
18
|
+
|
|
19
|
+
Special case: `JOIN 0` leaves all channels.
|
|
20
|
+
|
|
21
|
+
### Server Response to JOIN
|
|
22
|
+
|
|
23
|
+
On successful join, server sends:
|
|
24
|
+
1. `:yournick!user@host JOIN #channel` - Confirmation
|
|
25
|
+
2. `332 RPL_TOPIC` - Channel topic (if set)
|
|
26
|
+
3. `333 RPL_TOPICWHOTIME` - Who set topic and when (optional)
|
|
27
|
+
4. `353 RPL_NAMREPLY` - List of users in channel (may be multiple)
|
|
28
|
+
5. `366 RPL_ENDOFNAMES` - End of names list
|
|
29
|
+
|
|
30
|
+
### JOIN Errors
|
|
31
|
+
|
|
32
|
+
- 403 ERR_NOSUCHCHANNEL - Invalid channel name
|
|
33
|
+
- 405 ERR_TOOMANYCHANNELS - Client limit reached
|
|
34
|
+
- 471 ERR_CHANNELISFULL - Channel is full (+l mode)
|
|
35
|
+
- 473 ERR_INVITEONLYCHAN - Channel is invite-only (+i mode)
|
|
36
|
+
- 474 ERR_BANNEDFROMCHAN - Banned from channel
|
|
37
|
+
- 475 ERR_BADCHANNELKEY - Wrong or missing key (+k mode)
|
|
38
|
+
|
|
39
|
+
### PART Command
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
client.part("#ruby")
|
|
43
|
+
client.part("#ruby", "Goodbye!") # With reason
|
|
44
|
+
client.part("#a,#b") # Multiple channels
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Format: `PART <channel>[,<channel>...] [:<reason>]`
|
|
48
|
+
|
|
49
|
+
### Server Response to PART
|
|
50
|
+
|
|
51
|
+
Server sends: `:yournick!user@host PART #channel :reason`
|
|
52
|
+
|
|
53
|
+
### PART Errors
|
|
54
|
+
|
|
55
|
+
- 403 ERR_NOSUCHCHANNEL - Channel doesn't exist
|
|
56
|
+
- 442 ERR_NOTONCHANNEL - Not in that channel
|
|
57
|
+
|
|
58
|
+
### Tracking Other Users
|
|
59
|
+
|
|
60
|
+
When other users JOIN/PART:
|
|
61
|
+
- `:othernick!user@host JOIN #channel`
|
|
62
|
+
- `:othernick!user@host PART #channel :reason`
|
|
63
|
+
|
|
64
|
+
Emit appropriate events for these.
|
|
65
|
+
|
|
66
|
+
## Models
|
|
67
|
+
|
|
68
|
+
Track joined channels:
|
|
69
|
+
```ruby
|
|
70
|
+
client.channels # => {"#ruby" => Yaic::Channel, ...}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Tests
|
|
74
|
+
|
|
75
|
+
### Integration Tests - JOIN
|
|
76
|
+
|
|
77
|
+
**Join single channel**
|
|
78
|
+
- Given: Connected client
|
|
79
|
+
- When: `client.join("#test")`
|
|
80
|
+
- Then: Receive JOIN confirmation, RPL_NAMREPLY, RPL_ENDOFNAMES
|
|
81
|
+
|
|
82
|
+
**Join channel with topic**
|
|
83
|
+
- Given: Connected client, #test has topic
|
|
84
|
+
- When: `client.join("#test")`
|
|
85
|
+
- Then: Receive RPL_TOPIC with topic text
|
|
86
|
+
|
|
87
|
+
**Join key-protected channel**
|
|
88
|
+
- Given: Connected client, #secret requires key "pass123"
|
|
89
|
+
- When: `client.join("#secret", "pass123")`
|
|
90
|
+
- Then: Successfully joins
|
|
91
|
+
|
|
92
|
+
**Join key-protected with wrong key**
|
|
93
|
+
- Given: Connected client, #secret requires key
|
|
94
|
+
- When: `client.join("#secret", "wrongkey")`
|
|
95
|
+
- Then: Receive 475 ERR_BADCHANNELKEY
|
|
96
|
+
|
|
97
|
+
**Join multiple channels**
|
|
98
|
+
- Given: Connected client
|
|
99
|
+
- When: `client.join("#a,#b,#c")`
|
|
100
|
+
- Then: Joined to all three channels
|
|
101
|
+
|
|
102
|
+
**Join non-existent channel creates it**
|
|
103
|
+
- Given: Connected client
|
|
104
|
+
- When: `client.join("#newchannel")`
|
|
105
|
+
- Then: Channel created, client is operator
|
|
106
|
+
|
|
107
|
+
### Integration Tests - PART
|
|
108
|
+
|
|
109
|
+
**Part single channel**
|
|
110
|
+
- Given: Client in #test
|
|
111
|
+
- When: `client.part("#test")`
|
|
112
|
+
- Then: Receive PART confirmation, channel removed from tracking
|
|
113
|
+
|
|
114
|
+
**Part with reason**
|
|
115
|
+
- Given: Client in #test
|
|
116
|
+
- When: `client.part("#test", "Going home")`
|
|
117
|
+
- Then: Reason included in PART message
|
|
118
|
+
|
|
119
|
+
**Part channel not in**
|
|
120
|
+
- Given: Client not in #other
|
|
121
|
+
- When: `client.part("#other")`
|
|
122
|
+
- Then: Receive 442 ERR_NOTONCHANNEL
|
|
123
|
+
|
|
124
|
+
### Integration Tests - Events
|
|
125
|
+
|
|
126
|
+
**Emit :join event on self join**
|
|
127
|
+
- Given: Client with :join handler
|
|
128
|
+
- When: Join #test
|
|
129
|
+
- Then: Handler called with channel="#test", user=self
|
|
130
|
+
|
|
131
|
+
**Emit :join event on other join**
|
|
132
|
+
- Given: Client in #test with :join handler
|
|
133
|
+
- When: Other user joins #test
|
|
134
|
+
- Then: Handler called with channel="#test", user=other
|
|
135
|
+
|
|
136
|
+
**Emit :part event on self part**
|
|
137
|
+
- Given: Client in #test with :part handler
|
|
138
|
+
- When: Part #test
|
|
139
|
+
- Then: Handler called with channel="#test", user=self
|
|
140
|
+
|
|
141
|
+
**Emit :part event on other part**
|
|
142
|
+
- Given: Client in #test with :part handler
|
|
143
|
+
- When: Other user parts #test
|
|
144
|
+
- Then: Handler called with channel="#test", user=other, reason
|
|
145
|
+
|
|
146
|
+
### Unit Tests
|
|
147
|
+
|
|
148
|
+
**Format JOIN**
|
|
149
|
+
- Given: channel = "#test"
|
|
150
|
+
- When: Build JOIN
|
|
151
|
+
- Then: Output = "JOIN #test\r\n"
|
|
152
|
+
|
|
153
|
+
**Format JOIN with key**
|
|
154
|
+
- Given: channel = "#test", key = "secret"
|
|
155
|
+
- When: Build JOIN
|
|
156
|
+
- Then: Output = "JOIN #test secret\r\n"
|
|
157
|
+
|
|
158
|
+
**Format PART**
|
|
159
|
+
- Given: channel = "#test"
|
|
160
|
+
- When: Build PART
|
|
161
|
+
- Then: Output = "PART #test\r\n"
|
|
162
|
+
|
|
163
|
+
**Format PART with reason**
|
|
164
|
+
- Given: channel = "#test", reason = "Bye all"
|
|
165
|
+
- When: Build PART
|
|
166
|
+
- Then: Output = "PART #test :Bye all\r\n"
|
|
167
|
+
|
|
168
|
+
**Parse JOIN event**
|
|
169
|
+
- Given: `:nick!u@h JOIN #test`
|
|
170
|
+
- When: Parse
|
|
171
|
+
- Then: event.channel = "#test", event.user.nick = "nick"
|
|
172
|
+
|
|
173
|
+
**Parse PART event**
|
|
174
|
+
- Given: `:nick!u@h PART #test :Later`
|
|
175
|
+
- When: Parse
|
|
176
|
+
- Then: event.channel = "#test", event.user.nick = "nick", event.reason = "Later"
|
|
177
|
+
|
|
178
|
+
## Implementation Notes
|
|
179
|
+
|
|
180
|
+
- Track which channels client is in via `client.channels` hash
|
|
181
|
+
- Update channel user list on JOIN/PART from others
|
|
182
|
+
- RPL_NAMREPLY may come in multiple messages - collect until RPL_ENDOFNAMES
|
|
183
|
+
- Channel names start with # or & (check CHANTYPES from ISUPPORT)
|
|
184
|
+
|
|
185
|
+
## Dependencies
|
|
186
|
+
|
|
187
|
+
- Requires `01-message-parsing.md`
|
|
188
|
+
- Requires `02-connection-socket.md`
|
|
189
|
+
- Requires `03-registration.md`
|
|
190
|
+
- Requires `05-event-system.md`
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# QUIT Command
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement the QUIT command for gracefully disconnecting from the server, and handle QUIT messages from other users.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Sending QUIT
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client.quit
|
|
13
|
+
client.quit("Gone for lunch")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Format: `QUIT [:<reason>]`
|
|
17
|
+
|
|
18
|
+
After sending QUIT:
|
|
19
|
+
1. Server sends ERROR message
|
|
20
|
+
2. Server closes connection
|
|
21
|
+
3. Client should close socket and clean up
|
|
22
|
+
|
|
23
|
+
### Server Response to QUIT
|
|
24
|
+
|
|
25
|
+
Server sends: `ERROR :Closing Link: nick[host] (Quit: reason)`
|
|
26
|
+
|
|
27
|
+
Then closes the connection.
|
|
28
|
+
|
|
29
|
+
### Other Users Quitting
|
|
30
|
+
|
|
31
|
+
When another user quits: `:nick!user@host QUIT :reason`
|
|
32
|
+
|
|
33
|
+
Server prepends "Quit: " to reason when distributing.
|
|
34
|
+
|
|
35
|
+
Netsplits appear as: `:nick!user@host QUIT :server1 server2`
|
|
36
|
+
|
|
37
|
+
### Events
|
|
38
|
+
|
|
39
|
+
Emit `:quit` event with:
|
|
40
|
+
- `user` - Source of the quit
|
|
41
|
+
- `reason` - Quit message
|
|
42
|
+
|
|
43
|
+
Emit `:disconnect` event when own connection closes.
|
|
44
|
+
|
|
45
|
+
## Models
|
|
46
|
+
|
|
47
|
+
No new models.
|
|
48
|
+
|
|
49
|
+
## Tests
|
|
50
|
+
|
|
51
|
+
### Integration Tests
|
|
52
|
+
|
|
53
|
+
**Quit without reason**
|
|
54
|
+
- Given: Connected client
|
|
55
|
+
- When: `client.quit`
|
|
56
|
+
- Then: Server receives "QUIT", connection closes
|
|
57
|
+
|
|
58
|
+
**Quit with reason**
|
|
59
|
+
- Given: Connected client
|
|
60
|
+
- When: `client.quit("Bye!")`
|
|
61
|
+
- Then: Server receives "QUIT :Bye!", connection closes
|
|
62
|
+
|
|
63
|
+
**Receive other user quit**
|
|
64
|
+
- Given: Client in #test with :quit handler, other user in #test
|
|
65
|
+
- When: Other user quits
|
|
66
|
+
- Then: Handler called with user=other, reason
|
|
67
|
+
|
|
68
|
+
**Detect netsplit quit**
|
|
69
|
+
- Given: Client with :quit handler
|
|
70
|
+
- When: Receive QUIT with reason "*.net *.split"
|
|
71
|
+
- Then: Handler receives netsplit-style reason
|
|
72
|
+
|
|
73
|
+
### Unit Tests
|
|
74
|
+
|
|
75
|
+
**Format QUIT**
|
|
76
|
+
- Given: No reason
|
|
77
|
+
- When: Build QUIT
|
|
78
|
+
- Then: Output = "QUIT\r\n"
|
|
79
|
+
|
|
80
|
+
**Format QUIT with reason**
|
|
81
|
+
- Given: reason = "Going away"
|
|
82
|
+
- When: Build QUIT
|
|
83
|
+
- Then: Output = "QUIT :Going away\r\n"
|
|
84
|
+
|
|
85
|
+
**Parse QUIT event**
|
|
86
|
+
- Given: `:nick!u@h QUIT :Quit: Leaving`
|
|
87
|
+
- When: Parse
|
|
88
|
+
- Then: event.user.nick = "nick", event.reason = "Quit: Leaving"
|
|
89
|
+
|
|
90
|
+
**Parse netsplit QUIT**
|
|
91
|
+
- Given: `:nick!u@h QUIT :hub.net leaf.net`
|
|
92
|
+
- When: Parse
|
|
93
|
+
- Then: event.reason = "hub.net leaf.net"
|
|
94
|
+
|
|
95
|
+
### State Tests
|
|
96
|
+
|
|
97
|
+
**State after quit**
|
|
98
|
+
- Given: Connected client
|
|
99
|
+
- When: Quit
|
|
100
|
+
- Then: State = :disconnected, channels cleared
|
|
101
|
+
|
|
102
|
+
**Cleanup after quit**
|
|
103
|
+
- Given: Client in multiple channels
|
|
104
|
+
- When: Quit
|
|
105
|
+
- Then: All channel tracking cleared
|
|
106
|
+
|
|
107
|
+
## Implementation Notes
|
|
108
|
+
|
|
109
|
+
- QUIT is the graceful way to disconnect
|
|
110
|
+
- Wait briefly for ERROR response before closing socket
|
|
111
|
+
- Remove quitting users from all tracked channels
|
|
112
|
+
- Consider implementing `client.disconnect` as alias
|
|
113
|
+
|
|
114
|
+
## Dependencies
|
|
115
|
+
|
|
116
|
+
- Requires `01-message-parsing.md`
|
|
117
|
+
- Requires `02-connection-socket.md`
|
|
118
|
+
- Requires `05-event-system.md`
|