feat: implement Tier 3 utility IRC commands
All checks were successful
check / check (push) Successful in 59s
All checks were successful
check / check (push) Successful in 59s
Implement all 7 utility IRC commands from issue #87: User commands: - USERHOST: quick lookup of user@host for up to 5 nicks (RPL 302) - VERSION: server version string using globals.Version (RPL 351) - ADMIN: server admin contact info (RPL 256-259) - INFO: server software info text (RPL 371/374) - TIME: server local time in RFC format (RPL 391) Oper commands: - KILL: forcibly disconnect a user (requires is_oper), broadcasts QUIT to all shared channels, cleans up sessions - WALLOPS: broadcast message to all users with +w usermode (requires is_oper) Supporting changes: - Add is_wallops column to sessions table in 001_initial.sql - Add user mode +w tracking via MODE nick +w/-w - User mode queries now return actual modes (+o, +w) - MODE -o allows de-opering yourself; MODE +o rejected - MODE for other users returns ERR_USERSDONTMATCH (502) - Extract dispatch helpers to reduce dispatchCommand complexity Tests cover all commands including error cases, oper checks, user mode set/unset, KILL broadcast, WALLOPS delivery, and edge cases (self-kill, nonexistent users, missing params). closes #87
This commit is contained in:
@@ -2367,3 +2367,132 @@ func (database *Database) SetChannelUserLimit(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSessionWallops sets the wallops (+w) flag on a
|
||||
// session.
|
||||
func (database *Database) SetSessionWallops(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
enabled bool,
|
||||
) error {
|
||||
val := 0
|
||||
if enabled {
|
||||
val = 1
|
||||
}
|
||||
|
||||
_, err := database.conn.ExecContext(
|
||||
ctx,
|
||||
`UPDATE sessions SET is_wallops = ? WHERE id = ?`,
|
||||
val, sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set session wallops: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
if scanErr := rows.Scan(&sessionID); scanErr != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"scan wallops session: %w", scanErr,
|
||||
)
|
||||
}
|
||||
|
||||
ids = append(ids, sessionID)
|
||||
}
|
||||
|
||||
if err := rows.Err(); 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. Only nicks that exist are returned.
|
||||
func (database *Database) GetUserhostInfo(
|
||||
ctx context.Context,
|
||||
nicks []string,
|
||||
) ([]UserhostInfo, error) {
|
||||
if len(nicks) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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 err != nil {
|
||||
continue // nick not found, skip
|
||||
}
|
||||
|
||||
results = append(results, info)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user