Files
neoirc/internal/handlers/utility.go
T
clawbot 0f2e63d0cc
check / check (push) Waiting to run
Add the tier 3 utility IRC commands (closes #87)
USERHOST, VERSION, ADMIN, INFO, TIME, KILL and WALLOPS work on the
HTTP API and on the IRC listener. The USERHOST reply, the VERSION,
ADMIN 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,
without the operator waiting on it. 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
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
2026-10-08 07:31:37 +02:00

306 lines
6.4 KiB
Go

package handlers
import (
"net/http"
"strings"
"time"
"sneak.berlin/go/neoirc/pkg/irc"
)
// dispatchOperCommand routes the server operator commands
// OPER, KILL and WALLOPS to their handlers.
func (hdlr *Handlers) dispatchOperCommand(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick, command string,
bodyLines func() []string,
) {
switch command {
case irc.CmdOper:
hdlr.handleOper(
writer, request,
sessionID, clientID, nick, bodyLines,
)
case irc.CmdKill:
hdlr.handleKill(
writer, request,
sessionID, clientID, nick, bodyLines,
)
case irc.CmdWallops:
hdlr.handleWallops(
writer, request,
sessionID, clientID, nick, bodyLines,
)
}
}
// handleUserhost handles USERHOST: user@host for each of
// up to five nicks given in the body.
func (hdlr *Handlers) handleUserhost(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick string,
bodyLines func() []string,
) {
lines := bodyLines()
if len(lines) == 0 {
hdlr.respondIRCError(
writer, request, clientID, sessionID,
irc.ErrNeedMoreParams, nick,
[]string{irc.CmdUserhost},
"Not enough parameters",
)
return
}
reply, err := hdlr.svc.UserhostReply(
request.Context(), lines, hdlr.serverName(),
)
if hdlr.handleServiceError(
writer, request, clientID, sessionID, nick, err,
) {
return
}
hdlr.enqueueNumeric(
request.Context(), clientID,
irc.RplUserHost, nick, nil, reply,
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{statusKey: "ok"},
http.StatusOK)
}
// handleVersion handles VERSION.
func (hdlr *Handlers) handleVersion(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick string,
) {
hdlr.enqueueNumeric(
request.Context(), clientID, irc.RplVersion, nick,
[]string{
hdlr.svc.ServerVersion() + ".",
hdlr.serverName(),
},
"",
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{statusKey: "ok"},
http.StatusOK)
}
// handleAdmin handles ADMIN.
func (hdlr *Handlers) handleAdmin(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick string,
) {
ctx := request.Context()
srvName := hdlr.serverName()
location1, location2, email := hdlr.svc.AdminLines(srvName)
hdlr.enqueueNumeric(
ctx, clientID, irc.RplAdminMe, nick,
[]string{srvName}, "Administrative info",
)
hdlr.enqueueNumeric(
ctx, clientID, irc.RplAdminLoc1, nick, nil, location1,
)
hdlr.enqueueNumeric(
ctx, clientID, irc.RplAdminLoc2, nick, nil, location2,
)
hdlr.enqueueNumeric(
ctx, clientID, irc.RplAdminEmail, nick, nil, email,
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{statusKey: "ok"},
http.StatusOK)
}
// handleInfo handles INFO.
func (hdlr *Handlers) handleInfo(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick string,
) {
ctx := request.Context()
for _, line := range hdlr.svc.InfoLines() {
hdlr.enqueueNumeric(
ctx, clientID, irc.RplInfo, nick, nil, line,
)
}
hdlr.enqueueNumeric(
ctx, clientID, irc.RplEndOfInfo, nick, nil,
"End of /INFO list",
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{statusKey: "ok"},
http.StatusOK)
}
// handleTime handles TIME: the server's local time.
func (hdlr *Handlers) handleTime(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick string,
) {
hdlr.enqueueNumeric(
request.Context(), clientID, irc.RplTime, nick,
[]string{hdlr.serverName()},
time.Now().Format(time.RFC1123),
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{statusKey: "ok"},
http.StatusOK)
}
// handleKill handles KILL: the body is the target nick and
// an optional reason.
func (hdlr *Handlers) handleKill(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick string,
bodyLines func() []string,
) {
lines := bodyLines()
targetNick := ""
if len(lines) > 0 {
targetNick = strings.TrimSpace(lines[0])
}
if targetNick == "" {
hdlr.respondIRCError(
writer, request, clientID, sessionID,
irc.ErrNeedMoreParams, nick,
[]string{irc.CmdKill},
"Not enough parameters",
)
return
}
reason := ""
if len(lines) > 1 {
reason = lines[1]
}
err := hdlr.svc.KillUser(
request.Context(), sessionID, nick,
targetNick, reason,
)
if hdlr.handleServiceError(
writer, request, clientID, sessionID, nick, err,
) {
return
}
hdlr.respondJSON(writer, request,
map[string]string{statusKey: "ok"},
http.StatusOK)
}
// handleWallops handles WALLOPS: the body is the message.
func (hdlr *Handlers) handleWallops(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick string,
bodyLines func() []string,
) {
lines := bodyLines()
if len(lines) == 0 {
hdlr.respondIRCError(
writer, request, clientID, sessionID,
irc.ErrNeedMoreParams, nick,
[]string{irc.CmdWallops},
"Not enough parameters",
)
return
}
err := hdlr.svc.SendWallops(
request.Context(), sessionID, nick,
strings.Join(lines, " "),
)
if hdlr.handleServiceError(
writer, request, clientID, sessionID, nick, err,
) {
return
}
hdlr.respondJSON(writer, request,
map[string]string{statusKey: "ok"},
http.StatusOK)
}
// handleUserMode handles MODE for a nick: a query when the
// body is empty, otherwise a change by the mode string in
// the body. Only the caller's own nick, in any letter case,
// is allowed.
func (hdlr *Handlers) handleUserMode(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick, target string,
bodyLines func() []string,
) {
if !strings.EqualFold(target, nick) {
hdlr.respondIRCError(
writer, request, clientID, sessionID,
irc.ErrUsersDoNotMatch, nick, nil,
"Can't change mode for other users",
)
return
}
ctx := request.Context()
var (
modes string
err error
)
lines := bodyLines()
if len(lines) > 0 {
modes, err = hdlr.svc.ApplyUserMode(
ctx, sessionID, lines[0],
)
} else {
modes, err = hdlr.svc.QueryUserMode(ctx, sessionID)
}
if hdlr.handleServiceError(
writer, request, clientID, sessionID, nick, err,
) {
return
}
hdlr.enqueueNumeric(
ctx, clientID, irc.RplUmodeIs, nick, nil, modes,
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{statusKey: "ok"},
http.StatusOK)
}