feat: add OPER command and oper-only WHOIS client info
Some checks failed
check / check (push) Failing after 1m52s

- Add OPER command with NEOIRC_OPER_NAME/NEOIRC_OPER_PASSWORD config
- Add is_oper column to sessions table
- Add RPL_WHOISACTUALLY (338): show client IP/hostname to opers
- Add RPL_WHOISOPERATOR (313): show oper status in WHOIS
- Add GetOperCount for accurate LUSERS oper count
- Fix README schema: add ip/is_oper to sessions, ip/hostname to clients
- Add OPER command documentation and numeric references to README
- Refactor executeWhois to stay under funlen limit
- Add comprehensive tests for OPER auth, oper WHOIS, non-oper WHOIS

Closes #81
This commit is contained in:
clawbot
2026-03-17 10:48:04 -07:00
parent 16258722c7
commit 3571c50216
9 changed files with 807 additions and 36 deletions

View File

@@ -298,6 +298,75 @@ func (database *Database) GetClientHostInfo(
return &info, nil
}
// SetSessionOper sets the is_oper flag on a session.
func (database *Database) SetSessionOper(
ctx context.Context,
sessionID int64,
isOper bool,
) error {
val := 0
if isOper {
val = 1
}
_, err := database.conn.ExecContext(
ctx,
`UPDATE sessions SET is_oper = ? WHERE id = ?`,
val, sessionID,
)
if err != nil {
return fmt.Errorf("set session oper: %w", err)
}
return nil
}
// IsSessionOper returns whether the session has oper
// status.
func (database *Database) IsSessionOper(
ctx context.Context,
sessionID int64,
) (bool, error) {
var isOper int
err := database.conn.QueryRowContext(
ctx,
`SELECT is_oper FROM sessions WHERE id = ?`,
sessionID,
).Scan(&isOper)
if err != nil {
return false, fmt.Errorf(
"check session oper: %w", err,
)
}
return isOper != 0, nil
}
// GetLatestClientForSession returns the IP and hostname
// of the most recently created client for a session.
func (database *Database) GetLatestClientForSession(
ctx context.Context,
sessionID int64,
) (*ClientHostInfo, error) {
var info ClientHostInfo
err := database.conn.QueryRowContext(
ctx,
`SELECT ip, hostname FROM clients
WHERE session_id = ?
ORDER BY created_at DESC LIMIT 1`,
sessionID,
).Scan(&info.IP, &info.Hostname)
if err != nil {
return nil, fmt.Errorf(
"get latest client for session: %w", err,
)
}
return &info, nil
}
// GetChannelByName returns the channel ID for a name.
func (database *Database) GetChannelByName(
ctx context.Context,
@@ -951,6 +1020,26 @@ func (database *Database) GetUserCount(
return count, nil
}
// GetOperCount returns the number of sessions with oper
// status.
func (database *Database) GetOperCount(
ctx context.Context,
) (int64, error) {
var count int64
err := database.conn.QueryRowContext(
ctx,
"SELECT COUNT(*) FROM sessions WHERE is_oper = 1",
).Scan(&count)
if err != nil {
return 0, fmt.Errorf(
"get oper count: %w", err,
)
}
return count, nil
}
// ClientCountForSession returns the number of clients
// belonging to a session.
func (database *Database) ClientCountForSession(