feat: implement IRC numerics batch 2 — connection registration, channel ops, user queries (#59)
All checks were successful
check / check (push) Successful in 5s
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:
@@ -7,8 +7,10 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/irc"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -33,6 +35,7 @@ func generateToken() (string, error) {
|
||||
type IRCMessage struct {
|
||||
ID string `json:"id"`
|
||||
Command string `json:"command"`
|
||||
Code int `json:"code,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
@@ -42,6 +45,15 @@ type IRCMessage struct {
|
||||
DBID int64 `json:"-"`
|
||||
}
|
||||
|
||||
// isNumericCode returns true if s is exactly a 3-digit
|
||||
// IRC numeric reply code.
|
||||
func isNumericCode(s string) bool {
|
||||
return len(s) == 3 &&
|
||||
s[0] >= '0' && s[0] <= '9' &&
|
||||
s[1] >= '0' && s[1] <= '9' &&
|
||||
s[2] >= '0' && s[2] <= '9'
|
||||
}
|
||||
|
||||
// ChannelInfo is a lightweight channel representation.
|
||||
type ChannelInfo struct {
|
||||
ID int64 `json:"id"`
|
||||
@@ -717,6 +729,15 @@ func scanMessages(
|
||||
msg.DBID = qID
|
||||
lastQID = qID
|
||||
|
||||
if isNumericCode(msg.Command) {
|
||||
code, _ := strconv.Atoi(msg.Command)
|
||||
msg.Code = code
|
||||
|
||||
if name := irc.Name(code); name != "" {
|
||||
msg.Command = name
|
||||
}
|
||||
}
|
||||
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
|
||||
@@ -953,3 +974,125 @@ func (database *Database) GetSessionChannels(
|
||||
|
||||
return scanChannels(rows)
|
||||
}
|
||||
|
||||
// GetChannelCount returns the total number of channels.
|
||||
func (database *Database) GetChannelCount(
|
||||
ctx context.Context,
|
||||
) (int64, error) {
|
||||
var count int64
|
||||
|
||||
err := database.conn.QueryRowContext(
|
||||
ctx,
|
||||
"SELECT COUNT(*) FROM channels",
|
||||
).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf(
|
||||
"get channel count: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ChannelInfoFull contains extended channel information.
|
||||
type ChannelInfoFull struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Topic string `json:"topic"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
}
|
||||
|
||||
// ListAllChannelsWithCounts returns every channel
|
||||
// with its member count.
|
||||
func (database *Database) ListAllChannelsWithCounts(
|
||||
ctx context.Context,
|
||||
) ([]ChannelInfoFull, error) {
|
||||
rows, err := database.conn.QueryContext(ctx,
|
||||
`SELECT c.id, c.name, c.topic,
|
||||
COUNT(cm.session_id) AS member_count
|
||||
FROM channels c
|
||||
LEFT JOIN channel_members cm
|
||||
ON cm.channel_id = c.id
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"list channels with counts: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var out []ChannelInfoFull
|
||||
|
||||
for rows.Next() {
|
||||
var chanInfo ChannelInfoFull
|
||||
|
||||
err = rows.Scan(
|
||||
&chanInfo.ID, &chanInfo.Name,
|
||||
&chanInfo.Topic, &chanInfo.MemberCount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"scan channel full: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
out = append(out, chanInfo)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rows error: %w", err)
|
||||
}
|
||||
|
||||
if out == nil {
|
||||
out = []ChannelInfoFull{}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetChannelCreatedAt returns the creation time of a
|
||||
// channel.
|
||||
func (database *Database) GetChannelCreatedAt(
|
||||
ctx context.Context,
|
||||
channelID int64,
|
||||
) (time.Time, error) {
|
||||
var createdAt time.Time
|
||||
|
||||
err := database.conn.QueryRowContext(
|
||||
ctx,
|
||||
"SELECT created_at FROM channels WHERE id = ?",
|
||||
channelID,
|
||||
).Scan(&createdAt)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf(
|
||||
"get channel created_at: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return createdAt, nil
|
||||
}
|
||||
|
||||
// GetSessionCreatedAt returns the creation time of a
|
||||
// session.
|
||||
func (database *Database) GetSessionCreatedAt(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
) (time.Time, error) {
|
||||
var createdAt time.Time
|
||||
|
||||
err := database.conn.QueryRowContext(
|
||||
ctx,
|
||||
"SELECT created_at FROM sessions WHERE id = ?",
|
||||
sessionID,
|
||||
).Scan(&createdAt)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf(
|
||||
"get session created_at: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return createdAt, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user