Add the tier 3 utility IRC commands (closes #87) #96

Merged
clawbot merged 2 commits from feature/87-tier3-utility-commands into next 2026-10-08 07:31:38 +02:00
8 changed files with 70 additions and 18 deletions
Showing only changes of commit c0eef92d23 - Show all commits
+3 -2
View File
@@ -1951,8 +1951,9 @@ func (hdlr *Handlers) handleQuit(
http.StatusOK)
}
// handleMode handles the MODE command for channels and
// users. Currently supports query-only (no mode changes).
// handleMode handles the MODE command: a target that starts
// with # is a channel, handled by handleChannelMode, and any
// other target a nick, handled by handleUserMode.
func (hdlr *Handlers) handleMode(
writer http.ResponseWriter,
request *http.Request,
+5 -7
View File
@@ -106,22 +106,20 @@ func (hdlr *Handlers) handleAdmin(
) {
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,
"neoirc server",
ctx, clientID, irc.RplAdminLoc1, nick, nil, location1,
)
hdlr.enqueueNumeric(
ctx, clientID, irc.RplAdminLoc2, nick, nil,
"IRC over HTTP",
ctx, clientID, irc.RplAdminLoc2, nick, nil, location2,
)
hdlr.enqueueNumeric(
ctx, clientID, irc.RplAdminEmail, nick, nil,
"admin@"+srvName,
ctx, clientID, irc.RplAdminEmail, nick, nil, email,
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
@@ -199,7 +197,7 @@ func (hdlr *Handlers) handleKill(
return
}
reason := "KILLed"
reason := ""
if len(lines) > 1 {
reason = lines[1]
}
+13 -2
View File
@@ -90,6 +90,16 @@ func TestUserhost(t *testing.T) {
t.Errorf("expected alice and bob, got %q", body)
}
msgs, lastID = sendAndPoll(tserver, token, lastID, map[string]any{
commandKey: irc.CmdUserhost,
bodyKey: []string{"ghost"},
})
body = numericBody(t, msgs, "302")
if body != "" {
t.Errorf("expected an empty reply for ghost, got %q", body)
}
msgs, _ = sendAndPoll(tserver, token, lastID, map[string]any{
commandKey: irc.CmdUserhost,
})
@@ -187,8 +197,8 @@ func TestKill(t *testing.T) {
commandKey: irc.CmdKill,
bodyKey: []string{"victim", "go away"},
})
if status != http.StatusOK {
t.Fatalf("expected 200, got %d: %v", status, result)
if status != http.StatusOK || result[statusKey] != "ok" {
t.Fatalf("expected 200 and status ok, got %d: %v", status, result)
}
msgs, _ = tserver.pollMessages(observerToken, observerLastID)
@@ -283,6 +293,7 @@ func TestUserMode(t *testing.T) {
{nick, "", "221", "+w"},
{strings.ToUpper(nick), "-w", "221", "+"},
{nick, "+z", "501", ""},
{nick, "+o", "501", ""},
{other, "", "502", ""},
{other, "-w", "502", ""},
} {
+6 -4
View File
@@ -1313,12 +1313,14 @@ func (c *Conn) handleVersion() {
// handleAdmin replies with the server's admin info.
func (c *Conn) handleAdmin() {
location1, location2, email := c.svc.AdminLines(c.serverSfx)
c.sendNumeric(
irc.RplAdminMe, c.serverSfx, "Administrative info",
)
c.sendNumeric(irc.RplAdminLoc1, "neoirc server")
c.sendNumeric(irc.RplAdminLoc2, "IRC over HTTP")
c.sendNumeric(irc.RplAdminEmail, "admin@"+c.serverSfx)
c.sendNumeric(irc.RplAdminLoc1, location1)
c.sendNumeric(irc.RplAdminLoc2, location2)
c.sendNumeric(irc.RplAdminEmail, email)
}
// handleInfo replies with the server's software info.
@@ -1352,7 +1354,7 @@ func (c *Conn) handleKill(
return
}
reason := "KILLed"
reason := ""
if len(msg.Params) > 1 {
reason = msg.Params[1]
}
+4 -2
View File
@@ -37,7 +37,9 @@ func newKillVictim(t *testing.T) (*ircserver.Conn, net.Conn) {
// TestDisconnectDoesNotBlockOnUnresponsiveVictim checks
// that Disconnect returns although the victim never reads,
// so a KILL cannot stall the operator who sent it.
// so a KILL cannot stall the operator who sent it. The
// limit is well below one KillWriteWindow, so a Disconnect
// that waits on any write to the victim fails.
func TestDisconnectDoesNotBlockOnUnresponsiveVictim(
t *testing.T,
) {
@@ -53,7 +55,7 @@ func TestDisconnectDoesNotBlockOnUnresponsiveVictim(
select {
case <-returned:
case <-time.After(testTimeout):
case <-time.After(ircserver.KillWriteWindow / 4):
t.Fatal("Disconnect waited for the victim to read")
}
}
+4
View File
@@ -56,6 +56,10 @@ func (s *Server) Listener() net.Listener {
return s.listener
}
// KillWriteWindow is how long Disconnect allows each write
// to a killed victim.
const KillWriteWindow = killWriteWindow
// NewTestConn returns a Conn for nick on tcpConn, without a
// server, database or service behind it.
func NewTestConn(
+15 -1
View File
@@ -119,6 +119,15 @@ func (s *Service) InfoLines() []string {
}
}
// AdminLines returns the texts of the RPL_ADMINLOC1,
// RPL_ADMINLOC2 and RPL_ADMINEMAIL replies, in that order,
// that ADMIN sends on both transports.
func (s *Service) AdminLines(
serverName string,
) (string, string, string) {
return "neoirc server", "IRC over HTTP", "admin@" + serverName
}
// UserhostReply returns the RPL_USERHOST text for the first
// five of the given nicks: "nick=+user@host" entries joined
// by spaces, with * after the nick of an operator and - in
@@ -199,7 +208,8 @@ func (s *Service) UnregisterWireConn(
// KillUser carries out an operator's KILL: the target's
// channel peers see it quit, its session is deleted, and
// its IRC connection, if it has one, is closed.
// its IRC connection, if it has one, is closed. An empty
// reason is given as "KILLed".
func (s *Service) KillUser(
ctx context.Context,
sessionID int64,
@@ -231,6 +241,10 @@ func (s *Service) KillUser(
}
}
if reason == "" {
reason = "KILLed"
}
quitReason := "Killed (" + nick + " (" + reason + "))"
s.BroadcastQuit(ctx, targetSID, targetNick, quitReason)
+20
View File
@@ -406,6 +406,16 @@ func TestQueryUserMode(t *testing.T) {
if modes != "+ow" {
t.Errorf("expected +ow, got %s", modes)
}
// A flag that cannot be read is an error, not a flag
// that is unset.
cancelled, cancel := context.WithCancel(ctx)
cancel()
_, err = env.svc.QueryUserMode(cancelled, sid)
if !errors.Is(err, context.Canceled) {
t.Errorf("expected the read's error, got %v", err)
}
}
func TestUserhostReply(t *testing.T) {
@@ -453,6 +463,16 @@ func TestUserhostReply(t *testing.T) {
if strings.Count(reply, "bob=") != 5 {
t.Errorf("want 5 entries for 6 nicks, got %q", reply)
}
// A nick that cannot be looked up is an error, not a nick
// with no session.
cancelled, cancel := context.WithCancel(ctx)
cancel()
_, err = env.svc.UserhostReply(cancelled, []string{"alice"}, "srv")
if !errors.Is(err, context.Canceled) {
t.Errorf("want the lookup's error, got %v", err)
}
}
// userModes is the stored state of a session's user mode