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

USERHOST, VERSION, ADMIN, INFO, TIME, KILL and WALLOPS on both the
HTTP API and the IRC listener, and user mode +w.

Model: opus-5-5
This commit is contained in:
2026-10-08 00:26:08 +00:00
parent b119b094f5
commit e28fa005ed
19 changed files with 3931 additions and 85 deletions
+228 -5
View File
@@ -345,7 +345,10 @@ func (c *Conn) handleQuit(msg *Message) {
c.send("ERROR :Closing Link: " + c.hostname +
" (Quit: " + reason + ")")
c.mu.Lock()
c.closed = true
c.mu.Unlock()
}
// handleTopic gets or sets a channel topic via the shared
@@ -427,7 +430,7 @@ func (c *Conn) handleMode(
if strings.HasPrefix(target, "#") {
c.handleChannelMode(ctx, msg)
} else {
c.handleUserMode(msg)
c.handleUserMode(ctx, msg)
}
}
@@ -687,10 +690,13 @@ func (c *Conn) applyChannelModes(
}
// handleUserMode handles MODE for users.
func (c *Conn) handleUserMode(msg *Message) {
func (c *Conn) handleUserMode(
ctx context.Context,
msg *Message,
) {
target := msg.Params[0]
if !strings.EqualFold(target, c.nick) {
if !strings.EqualFold(target, c.currentNick()) {
c.sendNumeric(
irc.ErrUsersDoNotMatch,
"Can't change mode for other users",
@@ -699,8 +705,48 @@ func (c *Conn) handleUserMode(msg *Message) {
return
}
// We don't support user modes beyond the basics.
c.sendNumeric(irc.RplUmodeIs, "+")
// Mode query (no mode string).
if len(msg.Params) < 2 { //nolint:mnd
modes, err := c.svc.QueryUserMode(
ctx, c.sessionID,
)
if err != nil {
c.log.Error(
"query user mode failed", "error", err,
)
c.sendNumeric(
irc.ErrUmodeUnknownFlag,
"Unable to read user modes",
)
return
}
c.sendNumeric(irc.RplUmodeIs, modes)
return
}
newModes, err := c.svc.ApplyUserMode(
ctx, c.sessionID, msg.Params[1],
)
if err != nil {
var ircErr *service.IRCError
if errors.As(err, &ircErr) {
c.sendNumeric(ircErr.Code, ircErr.Message)
return
}
c.sendNumeric(
irc.ErrUmodeUnknownFlag,
"Unknown MODE flag",
)
return
}
c.sendNumeric(irc.RplUmodeIs, newModes)
}
// handleNames replies with channel member list.
@@ -1292,3 +1338,180 @@ func (c *Conn) handleUserhost(
strings.Join(replies, " "),
)
}
// handleVersion replies with the server version string.
func (c *Conn) handleVersion() {
c.sendNumeric(
irc.RplVersion,
c.svc.ServerVersion()+".", c.serverSfx,
"",
)
}
// handleAdmin replies with server admin info.
func (c *Conn) handleAdmin() {
srvName := c.serverSfx
c.sendNumeric(
irc.RplAdminMe,
srvName, "Administrative info",
)
c.sendNumeric(
irc.RplAdminLoc1,
"neoirc server",
)
c.sendNumeric(
irc.RplAdminLoc2,
"IRC over HTTP",
)
c.sendNumeric(
irc.RplAdminEmail,
"admin@"+srvName,
)
}
// handleInfo replies with server software info.
func (c *Conn) handleInfo() {
for _, line := range c.svc.InfoLines() {
c.sendNumeric(irc.RplInfo, line)
}
c.sendNumeric(
irc.RplEndOfInfo,
"End of /INFO list",
)
}
// handleTime replies with the server's current time.
func (c *Conn) handleTime() {
srvName := c.serverSfx
c.sendNumeric(
irc.RplTime,
srvName, time.Now().Format(time.RFC1123),
)
}
// handleKillCmd forcibly disconnects a target user (oper
// only).
func (c *Conn) handleKillCmd(
ctx context.Context,
msg *Message,
) {
isOper, err := c.database.IsSessionOper(
ctx, c.sessionID,
)
if err != nil || !isOper {
c.sendNumeric(
irc.ErrNoPrivileges,
"Permission Denied- "+
"You're not an IRC operator",
)
return
}
if len(msg.Params) < 1 {
c.sendNumeric(
irc.ErrNeedMoreParams,
"KILL", "Not enough parameters",
)
return
}
targetNick := msg.Params[0]
reason := "KILLed"
if len(msg.Params) > 1 {
reason = msg.Params[1]
}
killerNick := c.currentNick()
if strings.EqualFold(targetNick, killerNick) {
c.sendNumeric(
irc.ErrCantKillServer,
"You cannot KILL yourself",
)
return
}
targetSID, lookupErr := c.database.GetSessionByNick(
ctx, targetNick,
)
if lookupErr != nil {
c.sendNumeric(
irc.ErrNoSuchNick,
targetNick, "No such nick/channel",
)
return
}
quitReason := "Killed (" + killerNick +
" (" + reason + "))"
// KillSession broadcasts the QUIT, deletes the session
// and disconnects the victim's wire connection.
c.svc.KillSession(
ctx, targetSID, targetNick, quitReason,
)
}
// handleWallopsCmd broadcasts to all +w users (oper only).
func (c *Conn) handleWallopsCmd(
ctx context.Context,
msg *Message,
) {
isOper, err := c.database.IsSessionOper(
ctx, c.sessionID,
)
if err != nil || !isOper {
c.sendNumeric(
irc.ErrNoPrivileges,
"Permission Denied- "+
"You're not an IRC operator",
)
return
}
if len(msg.Params) < 1 {
c.sendNumeric(
irc.ErrNeedMoreParams,
"WALLOPS", "Not enough parameters",
)
return
}
message := msg.Params[0]
wallopsSIDs, wallErr := c.database.
GetWallopsSessionIDs(ctx)
if wallErr != nil {
c.log.Error(
"get wallops sessions failed",
"error", wallErr,
)
return
}
if len(wallopsSIDs) > 0 {
body, mErr := json.Marshal([]string{message})
if mErr != nil {
return
}
_, _, _ = c.svc.FanOut(
ctx, irc.CmdWallops, c.currentNick(), "*",
nil, body, nil, wallopsSIDs,
)
}
}