Add the tier 3 utility IRC commands (closes #87)
check / check (push) Waiting to run

USERHOST, VERSION, ADMIN, INFO, TIME, KILL and WALLOPS work on the
HTTP API and on the IRC listener. The USERHOST reply, the VERSION and
INFO text, KILL and WALLOPS are each one service function that both
transports call. User mode +w selects who receives WALLOPS. MODE on
your own nick queries or changes your user modes through one parser
on both transports: the whole mode string is checked before anything
is stored, and both flags are stored in one transaction. KILL closes
the victim's IRC connection after sending it KILL and ERROR. The
sessions table gains is_wallops in 001_initial.sql, as the pre-1.0
rule in REPO_POLICIES.md asks.

Model: opus-5-5
This commit is contained in:
2026-10-08 01:13:36 +00:00
parent b119b094f5
commit a7faabb68f
19 changed files with 2498 additions and 127 deletions
+170
View File
@@ -7,6 +7,7 @@ import (
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
@@ -2423,3 +2424,172 @@ func (database *Database) SetChannelUserLimit(
return nil
}
// SetSessionUserModes sets a session's wallops (+w) and
// oper (+o) flags in one transaction, so that either both
// changes are stored or neither is. A nil flag is left as
// it is.
func (database *Database) SetSessionUserModes(
ctx context.Context,
sessionID int64,
wallops *bool,
oper *bool,
) error {
transaction, err := database.conn.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
if wallops != nil {
_, err = transaction.ExecContext(
ctx,
`UPDATE sessions SET is_wallops = ? WHERE id = ?`,
boolToInt(*wallops), sessionID,
)
if err != nil {
_ = transaction.Rollback()
return fmt.Errorf("set session wallops: %w", err)
}
}
if oper != nil {
_, err = transaction.ExecContext(
ctx,
`UPDATE sessions SET is_oper = ? WHERE id = ?`,
boolToInt(*oper), sessionID,
)
if err != nil {
_ = transaction.Rollback()
return fmt.Errorf("set session oper: %w", err)
}
}
err = transaction.Commit()
if err != nil {
return fmt.Errorf("commit user modes: %w", err)
}
return nil
}
// boolToInt returns the 0 or 1 that the schema stores for a
// boolean.
func boolToInt(value bool) int {
if value {
return 1
}
return 0
}
// IsSessionWallops returns whether the session has the
// wallops (+w) usermode set.
func (database *Database) IsSessionWallops(
ctx context.Context,
sessionID int64,
) (bool, error) {
var isWallops int
err := database.conn.QueryRowContext(
ctx,
`SELECT is_wallops FROM sessions WHERE id = ?`,
sessionID,
).Scan(&isWallops)
if err != nil {
return false, fmt.Errorf(
"check session wallops: %w", err,
)
}
return isWallops != 0, nil
}
// GetWallopsSessionIDs returns all session IDs that have
// the wallops (+w) usermode set.
func (database *Database) GetWallopsSessionIDs(
ctx context.Context,
) ([]int64, error) {
rows, err := database.conn.QueryContext(
ctx,
`SELECT id FROM sessions WHERE is_wallops = 1`,
)
if err != nil {
return nil, fmt.Errorf(
"get wallops sessions: %w", err,
)
}
defer func() { _ = rows.Close() }()
var ids []int64
for rows.Next() {
var sessionID int64
err = rows.Scan(&sessionID)
if err != nil {
return nil, fmt.Errorf(
"scan wallops session: %w", err,
)
}
ids = append(ids, sessionID)
}
err = rows.Err()
if err != nil {
return nil, fmt.Errorf(
"iterate wallops sessions: %w", err,
)
}
return ids, nil
}
// UserhostInfo holds the data needed for RPL_USERHOST.
type UserhostInfo struct {
Nick string
Username string
Hostname string
IsOper bool
AwayMessage string
}
// GetUserhostInfo returns USERHOST info for the given
// nicks. Nicks with no session are left out.
func (database *Database) GetUserhostInfo(
ctx context.Context,
nicks []string,
) ([]UserhostInfo, error) {
results := make([]UserhostInfo, 0, len(nicks))
for _, nick := range nicks {
var info UserhostInfo
err := database.conn.QueryRowContext(
ctx,
`SELECT nick, username, hostname,
is_oper, away_message
FROM sessions WHERE nick = ?`,
nick,
).Scan(
&info.Nick, &info.Username, &info.Hostname,
&info.IsOper, &info.AwayMessage,
)
if errors.Is(err, sql.ErrNoRows) {
continue
}
if err != nil {
return nil, fmt.Errorf(
"userhost lookup %q: %w", nick, err,
)
}
results = append(results, info)
}
return results, nil
}