feat: implement IRC numerics batch 2 — connection registration, channel ops, user queries (#59)
All checks were successful
check / check (push) Successful in 5s

## Summary

Implements the remaining important/commonly-used IRC numeric reply codes, as requested in [issue #52](#52).

### Connection Registration (001-005)
- **002 RPL_YOURHOST** — "Your host is <server>, running version <ver>"
- **003 RPL_CREATED** — "This server was created <date>"
- **004 RPL_MYINFO** — "<server> <version> <usermodes> <chanmodes>"
- **005 RPL_ISUPPORT** — CHANTYPES=#, NICKLEN=32, CHANMODES, NETWORK=neoirc, CASEMAPPING=ascii

All sent automatically after RPL_WELCOME during session creation/login.

### Server Statistics (251-255)
- **251 RPL_LUSERCLIENT** — user count
- **252 RPL_LUSEROP** — operator count
- **254 RPL_LUSERCHANNELS** — channel count
- **255 RPL_LUSERME** — local client count

Sent during connection registration and available via LUSERS command.

### Channel Operations
- **MODE command** — query channel modes (324 RPL_CHANNELMODEIS + 329 RPL_CREATIONTIME) and user modes (221 RPL_UMODEIS)
- **NAMES command** — query channel member list (reuses 353/366)
- **LIST command** — list all channels with member counts (322 RPL_LIST + 323 end)

### User Queries
- **WHOIS command** — 311 RPL_WHOISUSER, 312 RPL_WHOISSERVER, 319 RPL_WHOISCHANNELS, 318 RPL_ENDOFWHOIS
- **WHO command** — 352 RPL_WHOREPLY, 315 RPL_ENDOFWHO

### Database Additions
- `GetChannelCount()` — total channel count for LUSERS
- `ListAllChannelsWithCounts()` — channels with member counts for LIST
- `GetChannelCreatedAt()` — channel creation time for RPL_CREATIONTIME
- `GetSessionCreatedAt()` — session creation time

### Other Changes
- Added `StartTime` to `Globals` struct for RPL_CREATED
- Updated README with comprehensive documentation of all new commands and numerics
- Updated roadmap to reflect implemented features

`docker build .` passes (lint, tests, build all green).

closes [#52](#52)

<!-- session: agent:sdlc-manager:subagent:1f3dcab8-ad6a-4c4c-af72-34a617640c9d -->

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: clawbot <clawbot@git.eeqj.de>
Reviewed-on: #59
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
Co-committed-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #59.
This commit is contained in:
2026-03-10 00:53:46 +01:00
committed by Jeffrey Paul
parent 47fb089969
commit 946f208ac2
9 changed files with 1128 additions and 320 deletions

View File

@@ -13,6 +13,8 @@ import (
"strconv"
"strings"
"time"
"git.eeqj.de/sneak/neoirc/internal/irc"
)
const (
@@ -168,7 +170,7 @@ func (client *Client) PollMessages(
func (client *Client) JoinChannel(channel string) error {
return client.SendMessage(
&Message{ //nolint:exhaustruct // only command+to needed
Command: "JOIN", To: channel,
Command: irc.CmdJoin, To: channel,
},
)
}
@@ -177,7 +179,7 @@ func (client *Client) JoinChannel(channel string) error {
func (client *Client) PartChannel(channel string) error {
return client.SendMessage(
&Message{ //nolint:exhaustruct // only command+to needed
Command: "PART", To: channel,
Command: irc.CmdPart, To: channel,
},
)
}

View File

@@ -9,6 +9,7 @@ import (
"time"
api "git.eeqj.de/sneak/neoirc/cmd/neoirc-cli/api"
"git.eeqj.de/sneak/neoirc/internal/irc"
)
const (
@@ -86,7 +87,7 @@ func (a *App) handleInput(text string) {
}
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
Command: "PRIVMSG",
Command: irc.CmdPrivmsg,
To: target,
Body: []string{text},
})
@@ -241,7 +242,7 @@ func (a *App) cmdNick(nick string) {
}
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
Command: "NICK",
Command: irc.CmdNick,
Body: []string{nick},
})
if err != nil {
@@ -376,7 +377,7 @@ func (a *App) cmdMsg(args string) {
}
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
Command: "PRIVMSG",
Command: irc.CmdPrivmsg,
To: target,
Body: []string{text},
})
@@ -434,7 +435,7 @@ func (a *App) cmdTopic(args string) {
if args == "" {
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
Command: "TOPIC",
Command: irc.CmdTopic,
To: target,
})
if err != nil {
@@ -447,7 +448,7 @@ func (a *App) cmdTopic(args string) {
}
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
Command: "TOPIC",
Command: irc.CmdTopic,
To: target,
Body: []string{args},
})
@@ -535,7 +536,7 @@ func (a *App) cmdMotd() {
}
err := a.client.SendMessage(
&api.Message{Command: "MOTD"}, //nolint:exhaustruct
&api.Message{Command: irc.CmdMotd}, //nolint:exhaustruct
)
if err != nil {
a.ui.AddStatus(fmt.Sprintf(
@@ -572,7 +573,7 @@ func (a *App) cmdWho(args string) {
err := a.client.SendMessage(
&api.Message{ //nolint:exhaustruct
Command: "WHO", To: channel,
Command: irc.CmdWho, To: channel,
},
)
if err != nil {
@@ -603,7 +604,7 @@ func (a *App) cmdWhois(args string) {
err := a.client.SendMessage(
&api.Message{ //nolint:exhaustruct
Command: "WHOIS", To: args,
Command: irc.CmdWhois, To: args,
},
)
if err != nil {
@@ -653,7 +654,7 @@ func (a *App) cmdQuit() {
if a.connected && a.client != nil {
_ = a.client.SendMessage(
&api.Message{Command: "QUIT"}, //nolint:exhaustruct
&api.Message{Command: irc.CmdQuit}, //nolint:exhaustruct
)
}
@@ -738,19 +739,19 @@ func (a *App) handleServerMessage(msg *api.Message) {
a.mu.Unlock()
switch msg.Command {
case "PRIVMSG":
case irc.CmdPrivmsg:
a.handlePrivmsgEvent(msg, timestamp, myNick)
case "JOIN":
case irc.CmdJoin:
a.handleJoinEvent(msg, timestamp)
case "PART":
case irc.CmdPart:
a.handlePartEvent(msg, timestamp)
case "QUIT":
case irc.CmdQuit:
a.handleQuitEvent(msg, timestamp)
case "NICK":
case irc.CmdNick:
a.handleNickEvent(msg, timestamp, myNick)
case "NOTICE":
case irc.CmdNotice:
a.handleNoticeEvent(msg, timestamp)
case "TOPIC":
case irc.CmdTopic:
a.handleTopicEvent(msg, timestamp)
default:
a.handleDefaultEvent(msg, timestamp)