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,109 @@
|
|
|
1
|
+
# NICK Change
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement changing nickname after registration and tracking nickname changes from other users.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Changing Own Nick
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client.nick("newnick")
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Format: `NICK <nickname>`
|
|
16
|
+
|
|
17
|
+
### Server Response
|
|
18
|
+
|
|
19
|
+
On success: `:oldnick!user@host NICK newnick`
|
|
20
|
+
|
|
21
|
+
Update internal state to track new nickname.
|
|
22
|
+
|
|
23
|
+
### Nick Change Errors
|
|
24
|
+
|
|
25
|
+
- 431 ERR_NONICKNAMEGIVEN - No nick provided
|
|
26
|
+
- 432 ERR_ERRONEUSNICKNAME - Invalid characters
|
|
27
|
+
- 433 ERR_NICKNAMEINUSE - Nick taken
|
|
28
|
+
- 436 ERR_NICKCOLLISION - Collision (rare)
|
|
29
|
+
|
|
30
|
+
### Other Users Changing Nick
|
|
31
|
+
|
|
32
|
+
When another user changes nick: `:oldnick!user@host NICK newnick`
|
|
33
|
+
|
|
34
|
+
Update user tracking in all shared channels.
|
|
35
|
+
|
|
36
|
+
### Events
|
|
37
|
+
|
|
38
|
+
Emit `:nick` event with:
|
|
39
|
+
- `old_nick` - Previous nickname
|
|
40
|
+
- `new_nick` - New nickname
|
|
41
|
+
- `user` - Source (optional, has old nick info)
|
|
42
|
+
|
|
43
|
+
## Models
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
client.nick # => current nickname (String)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Tests
|
|
50
|
+
|
|
51
|
+
### Integration Tests
|
|
52
|
+
|
|
53
|
+
**Change own nick**
|
|
54
|
+
- Given: Connected as "oldnick"
|
|
55
|
+
- When: `client.nick("newnick")`
|
|
56
|
+
- Then: Receive NICK confirmation, client.nick = "newnick"
|
|
57
|
+
|
|
58
|
+
**Nick in use**
|
|
59
|
+
- Given: Connected, "taken" nick exists
|
|
60
|
+
- When: `client.nick("taken")`
|
|
61
|
+
- Then: Receive 433 ERR_NICKNAMEINUSE
|
|
62
|
+
|
|
63
|
+
**Invalid nick**
|
|
64
|
+
- Given: Connected
|
|
65
|
+
- When: `client.nick("#invalid")`
|
|
66
|
+
- Then: Receive 432 ERR_ERRONEUSNICKNAME
|
|
67
|
+
|
|
68
|
+
**Other user changes nick**
|
|
69
|
+
- Given: Client in #test with "bob", :nick handler
|
|
70
|
+
- When: Bob changes nick to "robert"
|
|
71
|
+
- Then: Handler called with old_nick="bob", new_nick="robert"
|
|
72
|
+
|
|
73
|
+
### Unit Tests
|
|
74
|
+
|
|
75
|
+
**Format NICK**
|
|
76
|
+
- Given: nickname = "newnick"
|
|
77
|
+
- When: Build NICK
|
|
78
|
+
- Then: Output = "NICK newnick\r\n"
|
|
79
|
+
|
|
80
|
+
**Parse NICK event**
|
|
81
|
+
- Given: `:old!u@h NICK new`
|
|
82
|
+
- When: Parse
|
|
83
|
+
- Then: event.old_nick = "old", event.new_nick = "new"
|
|
84
|
+
|
|
85
|
+
**Track own nick change**
|
|
86
|
+
- Given: client.nick = "old"
|
|
87
|
+
- When: Receive `:old!u@h NICK new` from self
|
|
88
|
+
- Then: client.nick = "new"
|
|
89
|
+
|
|
90
|
+
### Channel User Tracking
|
|
91
|
+
|
|
92
|
+
**Update user in channels**
|
|
93
|
+
- Given: Client tracks "bob" in #test
|
|
94
|
+
- When: Bob changes nick to "robert"
|
|
95
|
+
- Then: #test user list shows "robert" not "bob"
|
|
96
|
+
|
|
97
|
+
## Implementation Notes
|
|
98
|
+
|
|
99
|
+
- Store current nick in client.nick
|
|
100
|
+
- On NICK event, check if source matches own nick to update self
|
|
101
|
+
- Update all channel user lists when any nick changes
|
|
102
|
+
- Nick comparison should be case-insensitive per server CASEMAPPING
|
|
103
|
+
|
|
104
|
+
## Dependencies
|
|
105
|
+
|
|
106
|
+
- Requires `01-message-parsing.md`
|
|
107
|
+
- Requires `02-connection-socket.md`
|
|
108
|
+
- Requires `03-registration.md`
|
|
109
|
+
- Requires `05-event-system.md`
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Channel TOPIC
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement getting and setting channel topics.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Getting Topic
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client.topic("#ruby") # Request current topic
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Format: `TOPIC <channel>`
|
|
16
|
+
|
|
17
|
+
Server responds with:
|
|
18
|
+
- 332 RPL_TOPIC - The topic text
|
|
19
|
+
- 333 RPL_TOPICWHOTIME - Who set it and when
|
|
20
|
+
- 331 RPL_NOTOPIC - No topic set
|
|
21
|
+
|
|
22
|
+
### Setting Topic
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
client.topic("#ruby", "Welcome to #ruby!")
|
|
26
|
+
client.topic("#ruby", "") # Clear topic
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Format: `TOPIC <channel> :<topic>`
|
|
30
|
+
|
|
31
|
+
### Topic Change Notification
|
|
32
|
+
|
|
33
|
+
When topic changes: `:nick!user@host TOPIC #channel :New topic`
|
|
34
|
+
|
|
35
|
+
### Topic Errors
|
|
36
|
+
|
|
37
|
+
- 442 ERR_NOTONCHANNEL - Not in channel (when getting)
|
|
38
|
+
- 482 ERR_CHANOPRIVSNEEDED - Not op (when setting protected topic)
|
|
39
|
+
|
|
40
|
+
### Events
|
|
41
|
+
|
|
42
|
+
Emit `:topic` event with:
|
|
43
|
+
- `channel` - Channel name
|
|
44
|
+
- `topic` - Topic text (may be nil if cleared)
|
|
45
|
+
- `setter` - Who set it (nick)
|
|
46
|
+
- `time` - When it was set (Time, optional)
|
|
47
|
+
|
|
48
|
+
## Models
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
channel.topic # => String or nil
|
|
52
|
+
channel.topic_setter # => String
|
|
53
|
+
channel.topic_time # => Time
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Tests
|
|
57
|
+
|
|
58
|
+
### Integration Tests
|
|
59
|
+
|
|
60
|
+
**Get topic**
|
|
61
|
+
- Given: Client in #test which has topic
|
|
62
|
+
- When: `client.topic("#test")`
|
|
63
|
+
- Then: Receive RPL_TOPIC with topic text
|
|
64
|
+
|
|
65
|
+
**Get topic when none set**
|
|
66
|
+
- Given: Client in #test with no topic
|
|
67
|
+
- When: `client.topic("#test")`
|
|
68
|
+
- Then: Receive RPL_NOTOPIC
|
|
69
|
+
|
|
70
|
+
**Set topic**
|
|
71
|
+
- Given: Client is op in #test
|
|
72
|
+
- When: `client.topic("#test", "New topic")`
|
|
73
|
+
- Then: Receive TOPIC confirmation, topic changed
|
|
74
|
+
|
|
75
|
+
**Clear topic**
|
|
76
|
+
- Given: Client is op in #test with topic
|
|
77
|
+
- When: `client.topic("#test", "")`
|
|
78
|
+
- Then: Topic cleared
|
|
79
|
+
|
|
80
|
+
**Set topic without permission**
|
|
81
|
+
- Given: Client in #test (not op), channel has +t mode
|
|
82
|
+
- When: `client.topic("#test", "New topic")`
|
|
83
|
+
- Then: Receive 482 ERR_CHANOPRIVSNEEDED
|
|
84
|
+
|
|
85
|
+
**Receive topic change**
|
|
86
|
+
- Given: Client in #test with :topic handler
|
|
87
|
+
- When: Op changes topic
|
|
88
|
+
- Then: Handler called with channel, new topic, setter
|
|
89
|
+
|
|
90
|
+
### Unit Tests
|
|
91
|
+
|
|
92
|
+
**Format TOPIC get**
|
|
93
|
+
- Given: channel = "#test"
|
|
94
|
+
- When: Build TOPIC (no text)
|
|
95
|
+
- Then: Output = "TOPIC #test\r\n"
|
|
96
|
+
|
|
97
|
+
**Format TOPIC set**
|
|
98
|
+
- Given: channel = "#test", topic = "Hello"
|
|
99
|
+
- When: Build TOPIC
|
|
100
|
+
- Then: Output = "TOPIC #test :Hello\r\n"
|
|
101
|
+
|
|
102
|
+
**Format TOPIC clear**
|
|
103
|
+
- Given: channel = "#test", topic = ""
|
|
104
|
+
- When: Build TOPIC
|
|
105
|
+
- Then: Output = "TOPIC #test :\r\n"
|
|
106
|
+
|
|
107
|
+
**Parse TOPIC event**
|
|
108
|
+
- Given: `:nick!u@h TOPIC #test :New topic`
|
|
109
|
+
- When: Parse
|
|
110
|
+
- Then: event.channel = "#test", event.topic = "New topic", event.setter = "nick"
|
|
111
|
+
|
|
112
|
+
**Parse RPL_TOPIC**
|
|
113
|
+
- Given: `:server 332 mynick #test :The topic`
|
|
114
|
+
- When: Parse
|
|
115
|
+
- Then: channel = "#test", topic = "The topic"
|
|
116
|
+
|
|
117
|
+
**Parse RPL_TOPICWHOTIME**
|
|
118
|
+
- Given: `:server 333 mynick #test setter 1234567890`
|
|
119
|
+
- When: Parse
|
|
120
|
+
- Then: setter = "setter", time = Time.at(1234567890)
|
|
121
|
+
|
|
122
|
+
### State Tracking
|
|
123
|
+
|
|
124
|
+
**Update channel topic on change**
|
|
125
|
+
- Given: Client tracking #test
|
|
126
|
+
- When: TOPIC message received
|
|
127
|
+
- Then: channel.topic updated
|
|
128
|
+
|
|
129
|
+
**Topic from JOIN**
|
|
130
|
+
- Given: Client joins #test with topic
|
|
131
|
+
- When: JOIN completes
|
|
132
|
+
- Then: channel.topic populated from RPL_TOPIC
|
|
133
|
+
|
|
134
|
+
## Implementation Notes
|
|
135
|
+
|
|
136
|
+
- Topic can be received at JOIN time (RPL_TOPIC) or via TOPIC command/event
|
|
137
|
+
- RPL_TOPICWHOTIME time is Unix timestamp
|
|
138
|
+
- Some channels have +t (topic protected) - only ops can change
|
|
139
|
+
- Empty string topic clears it
|
|
140
|
+
|
|
141
|
+
## Dependencies
|
|
142
|
+
|
|
143
|
+
- Requires `01-message-parsing.md`
|
|
144
|
+
- Requires `05-event-system.md`
|
|
145
|
+
- Requires `07-join-part.md` (topic received at join)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Channel KICK
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement kicking users from channels and handling being kicked.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Sending KICK
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client.kick("#ruby", "baduser")
|
|
13
|
+
client.kick("#ruby", "baduser", "No spamming")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Format: `KICK <channel> <nick> [:<reason>]`
|
|
17
|
+
|
|
18
|
+
Requires channel operator privileges.
|
|
19
|
+
|
|
20
|
+
### Server Response
|
|
21
|
+
|
|
22
|
+
Server confirms: `:kicker!user@host KICK #channel kicked :reason`
|
|
23
|
+
|
|
24
|
+
### Being Kicked
|
|
25
|
+
|
|
26
|
+
When client is kicked:
|
|
27
|
+
1. Receive KICK message
|
|
28
|
+
2. Remove channel from tracking
|
|
29
|
+
3. Emit :kick event
|
|
30
|
+
|
|
31
|
+
### Kick Errors
|
|
32
|
+
|
|
33
|
+
- 441 ERR_USERNOTINCHANNEL - Target not in channel
|
|
34
|
+
- 442 ERR_NOTONCHANNEL - Kicker not in channel
|
|
35
|
+
- 482 ERR_CHANOPRIVSNEEDED - Kicker not op
|
|
36
|
+
|
|
37
|
+
### Events
|
|
38
|
+
|
|
39
|
+
Emit `:kick` event with:
|
|
40
|
+
- `channel` - Channel name
|
|
41
|
+
- `user` - Who was kicked (nick)
|
|
42
|
+
- `by` - Who kicked (Source)
|
|
43
|
+
- `reason` - Kick reason
|
|
44
|
+
|
|
45
|
+
## Models
|
|
46
|
+
|
|
47
|
+
No new models.
|
|
48
|
+
|
|
49
|
+
## Tests
|
|
50
|
+
|
|
51
|
+
### Integration Tests
|
|
52
|
+
|
|
53
|
+
**Kick user**
|
|
54
|
+
- Given: Client is op in #test, "target" is in #test
|
|
55
|
+
- When: `client.kick("#test", "target")`
|
|
56
|
+
- Then: Target removed from channel
|
|
57
|
+
|
|
58
|
+
**Kick with reason**
|
|
59
|
+
- Given: Client is op in #test
|
|
60
|
+
- When: `client.kick("#test", "target", "Breaking rules")`
|
|
61
|
+
- Then: Reason included in KICK
|
|
62
|
+
|
|
63
|
+
**Kick without permission**
|
|
64
|
+
- Given: Client in #test (not op)
|
|
65
|
+
- When: `client.kick("#test", "target")`
|
|
66
|
+
- Then: Receive 482 ERR_CHANOPRIVSNEEDED
|
|
67
|
+
|
|
68
|
+
**Kick non-existent user**
|
|
69
|
+
- Given: Client is op in #test
|
|
70
|
+
- When: `client.kick("#test", "nobody")`
|
|
71
|
+
- Then: Receive 441 ERR_USERNOTINCHANNEL
|
|
72
|
+
|
|
73
|
+
**Receive kick (others)**
|
|
74
|
+
- Given: Client in #test with :kick handler
|
|
75
|
+
- When: Op kicks "baduser"
|
|
76
|
+
- Then: Handler called with channel, user="baduser", by=op
|
|
77
|
+
|
|
78
|
+
**Receive kick (self)**
|
|
79
|
+
- Given: Client in #test
|
|
80
|
+
- When: Op kicks client
|
|
81
|
+
- Then: :kick event emitted, #test removed from channels
|
|
82
|
+
|
|
83
|
+
### Unit Tests
|
|
84
|
+
|
|
85
|
+
**Format KICK**
|
|
86
|
+
- Given: channel = "#test", nick = "target"
|
|
87
|
+
- When: Build KICK
|
|
88
|
+
- Then: Output = "KICK #test target\r\n"
|
|
89
|
+
|
|
90
|
+
**Format KICK with reason**
|
|
91
|
+
- Given: channel = "#test", nick = "target", reason = "Bye"
|
|
92
|
+
- When: Build KICK
|
|
93
|
+
- Then: Output = "KICK #test target :Bye\r\n"
|
|
94
|
+
|
|
95
|
+
**Parse KICK event**
|
|
96
|
+
- Given: `:op!u@h KICK #test target :reason`
|
|
97
|
+
- When: Parse
|
|
98
|
+
- Then: event.channel = "#test", event.user = "target", event.by.nick = "op", event.reason = "reason"
|
|
99
|
+
|
|
100
|
+
### State Updates
|
|
101
|
+
|
|
102
|
+
**Remove kicked user from channel**
|
|
103
|
+
- Given: Client tracking "target" in #test
|
|
104
|
+
- When: KICK for "target"
|
|
105
|
+
- Then: "target" removed from #test user list
|
|
106
|
+
|
|
107
|
+
**Remove channel when self kicked**
|
|
108
|
+
- Given: Client in #test
|
|
109
|
+
- When: Kicked from #test
|
|
110
|
+
- Then: #test removed from client.channels
|
|
111
|
+
|
|
112
|
+
## Implementation Notes
|
|
113
|
+
|
|
114
|
+
- Kick requires op status (@) in channel
|
|
115
|
+
- Server may have kick reason length limits
|
|
116
|
+
- After being kicked, must rejoin if allowed
|
|
117
|
+
|
|
118
|
+
## Dependencies
|
|
119
|
+
|
|
120
|
+
- Requires `01-message-parsing.md`
|
|
121
|
+
- Requires `05-event-system.md`
|
|
122
|
+
- Requires `07-join-part.md` (channel tracking)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Channel NAMES
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement querying channel user lists. NAMES returns all users in a channel with their mode prefixes.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Requesting NAMES
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client.names("#ruby")
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Format: `NAMES <channel>`
|
|
16
|
+
|
|
17
|
+
### Server Response
|
|
18
|
+
|
|
19
|
+
- 353 RPL_NAMREPLY - User list (may be multiple)
|
|
20
|
+
- 366 RPL_ENDOFNAMES - End of list
|
|
21
|
+
|
|
22
|
+
RPL_NAMREPLY format: `:server 353 mynick = #channel :@op +voice regular`
|
|
23
|
+
|
|
24
|
+
User prefixes:
|
|
25
|
+
- `@` - Operator
|
|
26
|
+
- `+` - Voice
|
|
27
|
+
- `%` - Half-op (some servers)
|
|
28
|
+
- `~` - Owner (some servers)
|
|
29
|
+
- `&` - Admin (some servers)
|
|
30
|
+
|
|
31
|
+
### Events
|
|
32
|
+
|
|
33
|
+
Emit `:names` event with:
|
|
34
|
+
- `channel` - Channel name
|
|
35
|
+
- `users` - Array of {nick:, modes:} or similar
|
|
36
|
+
|
|
37
|
+
Or update channel.users and emit general update event.
|
|
38
|
+
|
|
39
|
+
## Models
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
channel.users # => Hash[String, Set[Symbol]]
|
|
43
|
+
# e.g., {"dan" => Set[:op], "bob" => Set[:voice]}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Tests
|
|
47
|
+
|
|
48
|
+
### Integration Tests
|
|
49
|
+
|
|
50
|
+
**Get names**
|
|
51
|
+
- Given: Client in #test with users
|
|
52
|
+
- When: `client.names("#test")`
|
|
53
|
+
- Then: Receive RPL_NAMREPLY with user list
|
|
54
|
+
|
|
55
|
+
**Names with prefixes**
|
|
56
|
+
- Given: #test has @op and +voice users
|
|
57
|
+
- When: `client.names("#test")`
|
|
58
|
+
- Then: Response shows prefixes
|
|
59
|
+
|
|
60
|
+
**Names at join**
|
|
61
|
+
- Given: Client joins #test
|
|
62
|
+
- When: JOIN completes
|
|
63
|
+
- Then: channel.users populated from NAMREPLY
|
|
64
|
+
|
|
65
|
+
**Multi-message names**
|
|
66
|
+
- Given: Channel with many users
|
|
67
|
+
- When: Request NAMES
|
|
68
|
+
- Then: Multiple RPL_NAMREPLY collected until RPL_ENDOFNAMES
|
|
69
|
+
|
|
70
|
+
### Unit Tests
|
|
71
|
+
|
|
72
|
+
**Parse RPL_NAMREPLY**
|
|
73
|
+
- Given: `:server 353 me = #test :@op +voice regular`
|
|
74
|
+
- When: Parse
|
|
75
|
+
- Then: Extract users with modes: op=>[:op], voice=>[:voice], regular=>[]
|
|
76
|
+
|
|
77
|
+
**Parse prefix @ (op)**
|
|
78
|
+
- Given: User string "@dan"
|
|
79
|
+
- When: Parse user
|
|
80
|
+
- Then: nick = "dan", modes = [:op]
|
|
81
|
+
|
|
82
|
+
**Parse prefix + (voice)**
|
|
83
|
+
- Given: User string "+bob"
|
|
84
|
+
- When: Parse user
|
|
85
|
+
- Then: nick = "bob", modes = [:voice]
|
|
86
|
+
|
|
87
|
+
**Parse multiple prefixes**
|
|
88
|
+
- Given: User string "@+admin"
|
|
89
|
+
- When: Parse user
|
|
90
|
+
- Then: nick = "admin", modes = [:op, :voice]
|
|
91
|
+
|
|
92
|
+
**Parse no prefix**
|
|
93
|
+
- Given: User string "regular"
|
|
94
|
+
- When: Parse user
|
|
95
|
+
- Then: nick = "regular", modes = []
|
|
96
|
+
|
|
97
|
+
**Collect until ENDOFNAMES**
|
|
98
|
+
- Given: Multiple RPL_NAMREPLY then RPL_ENDOFNAMES
|
|
99
|
+
- When: Process all
|
|
100
|
+
- Then: All users aggregated before emitting event
|
|
101
|
+
|
|
102
|
+
### State Updates
|
|
103
|
+
|
|
104
|
+
**Populate users on join**
|
|
105
|
+
- Given: Client joins #test
|
|
106
|
+
- When: NAMREPLY received
|
|
107
|
+
- Then: channel.users contains all listed users
|
|
108
|
+
|
|
109
|
+
**Update user modes**
|
|
110
|
+
- Given: Client tracking #test
|
|
111
|
+
- When: New NAMES requested and received
|
|
112
|
+
- Then: channel.users updated
|
|
113
|
+
|
|
114
|
+
## Implementation Notes
|
|
115
|
+
|
|
116
|
+
- NAMREPLY at JOIN is same as explicit NAMES command
|
|
117
|
+
- May receive multiple NAMREPLY messages - collect all before processing
|
|
118
|
+
- PREFIX in ISUPPORT defines which prefixes map to which modes
|
|
119
|
+
- Default: @ = op, + = voice
|
|
120
|
+
|
|
121
|
+
## Dependencies
|
|
122
|
+
|
|
123
|
+
- Requires `01-message-parsing.md`
|
|
124
|
+
- Requires `07-join-part.md` (NAMES comes with JOIN)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# MODE Command
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Implement user and channel mode queries and changes. This covers both viewing and setting modes.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### User Modes
|
|
10
|
+
|
|
11
|
+
Query own modes:
|
|
12
|
+
```ruby
|
|
13
|
+
client.mode(client.nick) # Get current modes
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Set own modes:
|
|
17
|
+
```ruby
|
|
18
|
+
client.mode(client.nick, "+i") # Set invisible
|
|
19
|
+
client.mode(client.nick, "-i") # Unset invisible
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Common user modes:
|
|
23
|
+
- `+i` - Invisible (hide from WHO unless sharing channel)
|
|
24
|
+
- `+w` - Receive wallops
|
|
25
|
+
- `+o` - Operator (set via OPER, not MODE)
|
|
26
|
+
|
|
27
|
+
### Channel Modes
|
|
28
|
+
|
|
29
|
+
Query channel modes:
|
|
30
|
+
```ruby
|
|
31
|
+
client.mode("#ruby") # Get channel modes
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Set channel modes (requires op):
|
|
35
|
+
```ruby
|
|
36
|
+
client.mode("#ruby", "+m") # Set moderated
|
|
37
|
+
client.mode("#ruby", "+o", "nick") # Give op to nick
|
|
38
|
+
client.mode("#ruby", "+k", "secret") # Set channel key
|
|
39
|
+
client.mode("#ruby", "-k") # Remove key
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Common channel modes:
|
|
43
|
+
- `+o nick` - Channel operator
|
|
44
|
+
- `+v nick` - Voice
|
|
45
|
+
- `+m` - Moderated (only +v/+o can speak)
|
|
46
|
+
- `+i` - Invite only
|
|
47
|
+
- `+k key` - Require key to join
|
|
48
|
+
- `+l limit` - User limit
|
|
49
|
+
- `+b mask` - Ban mask
|
|
50
|
+
- `+t` - Only ops can change topic
|
|
51
|
+
|
|
52
|
+
### Server Response
|
|
53
|
+
|
|
54
|
+
Mode query: `324 RPL_CHANNELMODEIS :server 324 mynick #chan +nt`
|
|
55
|
+
|
|
56
|
+
Mode change: `:nick!u@h MODE #chan +o target`
|
|
57
|
+
|
|
58
|
+
### Mode Errors
|
|
59
|
+
|
|
60
|
+
- 472 ERR_UNKNOWNMODE - Unknown mode character
|
|
61
|
+
- 482 ERR_CHANOPRIVSNEEDED - Need op to change channel mode
|
|
62
|
+
- 501 ERR_UMODEUNKNOWNFLAG - Unknown user mode
|
|
63
|
+
|
|
64
|
+
### Events
|
|
65
|
+
|
|
66
|
+
Emit `:mode` event with:
|
|
67
|
+
- `target` - Channel or nick
|
|
68
|
+
- `modes` - Mode string (e.g., "+o-v")
|
|
69
|
+
- `params` - Mode parameters array
|
|
70
|
+
|
|
71
|
+
## Models
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
channel.modes # => Hash[Symbol, Object]
|
|
75
|
+
# e.g., {moderated: true, key: "secret", limit: 50}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Tests
|
|
79
|
+
|
|
80
|
+
### Integration Tests - User Modes
|
|
81
|
+
|
|
82
|
+
**Get own modes**
|
|
83
|
+
- Given: Connected client
|
|
84
|
+
- When: `client.mode(client.nick)`
|
|
85
|
+
- Then: Receive 221 RPL_UMODEIS
|
|
86
|
+
|
|
87
|
+
**Set invisible**
|
|
88
|
+
- Given: Connected client
|
|
89
|
+
- When: `client.mode(client.nick, "+i")`
|
|
90
|
+
- Then: Mode confirmed, hidden from WHO
|
|
91
|
+
|
|
92
|
+
**Cannot set other user's modes**
|
|
93
|
+
- Given: Connected client
|
|
94
|
+
- When: `client.mode("other", "+i")`
|
|
95
|
+
- Then: Receive 502 ERR_USERSDONTMATCH
|
|
96
|
+
|
|
97
|
+
### Integration Tests - Channel Modes
|
|
98
|
+
|
|
99
|
+
**Get channel modes**
|
|
100
|
+
- Given: Client in #test
|
|
101
|
+
- When: `client.mode("#test")`
|
|
102
|
+
- Then: Receive 324 RPL_CHANNELMODEIS
|
|
103
|
+
|
|
104
|
+
**Set channel mode as op**
|
|
105
|
+
- Given: Client is op in #test
|
|
106
|
+
- When: `client.mode("#test", "+m")`
|
|
107
|
+
- Then: Mode confirmed
|
|
108
|
+
|
|
109
|
+
**Give op to user**
|
|
110
|
+
- Given: Client is op in #test, "target" in channel
|
|
111
|
+
- When: `client.mode("#test", "+o", "target")`
|
|
112
|
+
- Then: Target becomes op
|
|
113
|
+
|
|
114
|
+
**Set key**
|
|
115
|
+
- Given: Client is op in #test
|
|
116
|
+
- When: `client.mode("#test", "+k", "secret")`
|
|
117
|
+
- Then: Channel now requires key
|
|
118
|
+
|
|
119
|
+
**Mode without permission**
|
|
120
|
+
- Given: Client in #test (not op)
|
|
121
|
+
- When: `client.mode("#test", "+m")`
|
|
122
|
+
- Then: Receive 482 ERR_CHANOPRIVSNEEDED
|
|
123
|
+
|
|
124
|
+
### Unit Tests
|
|
125
|
+
|
|
126
|
+
**Format MODE query**
|
|
127
|
+
- Given: target = "#test"
|
|
128
|
+
- When: Build MODE (no modes)
|
|
129
|
+
- Then: Output = "MODE #test\r\n"
|
|
130
|
+
|
|
131
|
+
**Format MODE set**
|
|
132
|
+
- Given: target = "#test", modes = "+m"
|
|
133
|
+
- When: Build MODE
|
|
134
|
+
- Then: Output = "MODE #test +m\r\n"
|
|
135
|
+
|
|
136
|
+
**Format MODE with params**
|
|
137
|
+
- Given: target = "#test", modes = "+o", params = ["nick"]
|
|
138
|
+
- When: Build MODE
|
|
139
|
+
- Then: Output = "MODE #test +o nick\r\n"
|
|
140
|
+
|
|
141
|
+
**Parse MODE event**
|
|
142
|
+
- Given: `:op!u@h MODE #test +o target`
|
|
143
|
+
- When: Parse
|
|
144
|
+
- Then: target = "#test", modes = "+o", params = ["target"]
|
|
145
|
+
|
|
146
|
+
**Parse multi-mode**
|
|
147
|
+
- Given: `:op!u@h MODE #test +ov target1 target2`
|
|
148
|
+
- When: Parse
|
|
149
|
+
- Then: modes = "+ov", params = ["target1", "target2"]
|
|
150
|
+
|
|
151
|
+
### State Updates
|
|
152
|
+
|
|
153
|
+
**Track channel modes**
|
|
154
|
+
- Given: Client in #test
|
|
155
|
+
- When: MODE +m received
|
|
156
|
+
- Then: channel.modes[:moderated] = true
|
|
157
|
+
|
|
158
|
+
**Track user op status**
|
|
159
|
+
- Given: Client tracking "nick" in #test
|
|
160
|
+
- When: MODE +o nick received
|
|
161
|
+
- Then: "nick" has :op in user modes
|
|
162
|
+
|
|
163
|
+
## Implementation Notes
|
|
164
|
+
|
|
165
|
+
- Parse CHANMODES from ISUPPORT for parameter requirements
|
|
166
|
+
- Mode types: A (list), B (always param), C (param on set), D (no param)
|
|
167
|
+
- Multiple modes can be set at once: +ov nick1 nick2
|
|
168
|
+
- Track modes received both at join and via MODE messages
|
|
169
|
+
|
|
170
|
+
## Dependencies
|
|
171
|
+
|
|
172
|
+
- Requires `01-message-parsing.md`
|
|
173
|
+
- Requires `05-event-system.md`
|
|
174
|
+
- Requires `07-join-part.md` (channel tracking)
|