fix: address review findings — dynamic version, deduplicate KILL, update README
All checks were successful
check / check (push) Successful in 1m3s

This commit is contained in:
clawbot
2026-04-01 14:39:21 -07:00
parent 17479c4f44
commit 327ff37059
3 changed files with 35 additions and 80 deletions

View File

@@ -2307,8 +2307,8 @@ IRC_LISTEN_ADDR=
| Connection | `NICK`, `USER`, `PASS`, `QUIT`, `PING`/`PONG`, `CAP` | | Connection | `NICK`, `USER`, `PASS`, `QUIT`, `PING`/`PONG`, `CAP` |
| Channels | `JOIN`, `PART`, `MODE`, `TOPIC`, `NAMES`, `LIST`, `KICK`, `INVITE` | | Channels | `JOIN`, `PART`, `MODE`, `TOPIC`, `NAMES`, `LIST`, `KICK`, `INVITE` |
| Messaging | `PRIVMSG`, `NOTICE` | | Messaging | `PRIVMSG`, `NOTICE` |
| Info | `WHO`, `WHOIS`, `LUSERS`, `MOTD`, `AWAY` | | Info | `WHO`, `WHOIS`, `LUSERS`, `MOTD`, `AWAY`, `USERHOST`, `VERSION`, `ADMIN`, `INFO`, `TIME` |
| Operator | `OPER` (requires `NEOIRC_OPER_NAME` and `NEOIRC_OPER_PASSWORD`) | | Operator | `OPER`, `KILL`, `WALLOPS` (requires `NEOIRC_OPER_NAME` and `NEOIRC_OPER_PASSWORD`) |
### Protocol Details ### Protocol Details
@@ -2820,6 +2820,10 @@ guess is borne by the server (bcrypt), not the client.
login from additional devices via `POST /api/v1/login` login from additional devices via `POST /api/v1/login`
- [x] **Cookie-based auth** — HttpOnly cookies replace Bearer tokens for - [x] **Cookie-based auth** — HttpOnly cookies replace Bearer tokens for
all API authentication all API authentication
- [x] **Tier 3 utility commands** — USERHOST (302), VERSION (351), ADMIN
(256259), INFO (371/374), TIME (391), KILL (oper-only forced
disconnect), WALLOPS (oper-only broadcast to +w users)
- [x] **User mode +w** — wallops usermode via `MODE nick +w/-w`
### Future (1.0+) ### Future (1.0+)

View File

@@ -331,18 +331,12 @@ func (hdlr *Handlers) handleKill(
} }
lines := bodyLines() lines := bodyLines()
if len(lines) == 0 {
hdlr.respondIRCError(
writer, request, clientID, sessionID,
irc.ErrNeedMoreParams, nick,
[]string{irc.CmdKill},
"Not enough parameters",
)
return var targetNick string
if len(lines) > 0 {
targetNick = strings.TrimSpace(lines[0])
} }
targetNick := strings.TrimSpace(lines[0])
if targetNick == "" { if targetNick == "" {
hdlr.respondIRCError( hdlr.respondIRCError(
writer, request, clientID, sessionID, writer, request, clientID, sessionID,
@@ -383,8 +377,11 @@ func (hdlr *Handlers) handleKill(
return return
} }
hdlr.executeKillUser( quitReason := "Killed (" + nick + " (" + reason + "))"
request, targetSID, targetNick, nick, reason,
hdlr.svc.BroadcastQuit(
request.Context(), targetSID,
targetNick, quitReason,
) )
hdlr.respondJSON(writer, request, hdlr.respondJSON(writer, request,
@@ -392,71 +389,6 @@ func (hdlr *Handlers) handleKill(
http.StatusOK) http.StatusOK)
} }
// executeKillUser forcibly disconnects a user: broadcasts
// QUIT to their channels, parts all channels, and deletes
// the session.
func (hdlr *Handlers) executeKillUser(
request *http.Request,
targetSID int64,
targetNick, killerNick, reason string,
) {
ctx := request.Context()
quitMsg := "Killed (" + killerNick + " (" + reason + "))"
quitBody, err := json.Marshal([]string{quitMsg})
if err != nil {
hdlr.log.Error(
"marshal kill quit body", "error", err,
)
return
}
channels, _ := hdlr.params.Database.
GetSessionChannels(ctx, targetSID)
notified := map[int64]bool{}
var dbID int64
if len(channels) > 0 {
dbID, _, _ = hdlr.params.Database.InsertMessage(
ctx, irc.CmdQuit, targetNick, "",
nil, json.RawMessage(quitBody), nil,
)
}
for _, chanInfo := range channels {
memberIDs, _ := hdlr.params.Database.
GetChannelMemberIDs(ctx, chanInfo.ID)
for _, mid := range memberIDs {
if mid != targetSID && !notified[mid] {
notified[mid] = true
_ = hdlr.params.Database.EnqueueToSession(
ctx, mid, dbID,
)
hdlr.broker.Notify(mid)
}
}
_ = hdlr.params.Database.PartChannel(
ctx, chanInfo.ID, targetSID,
)
_ = hdlr.params.Database.DeleteChannelIfEmpty(
ctx, chanInfo.ID,
)
}
_ = hdlr.params.Database.DeleteSession(
ctx, targetSID,
)
}
// handleWallops handles the WALLOPS command. // handleWallops handles the WALLOPS command.
// Broadcasts a message to all users with +w usermode // Broadcasts a message to all users with +w usermode
// (oper only). // (oper only).

View File

@@ -8,10 +8,29 @@ import (
"strings" "strings"
"time" "time"
"sneak.berlin/go/neoirc/internal/globals"
"sneak.berlin/go/neoirc/internal/service" "sneak.berlin/go/neoirc/internal/service"
"sneak.berlin/go/neoirc/pkg/irc" "sneak.berlin/go/neoirc/pkg/irc"
) )
// versionString returns the server version for IRC
// responses, falling back to "neoirc-dev" when globals
// are not set (e.g. during tests).
func versionString() string {
name := globals.Appname
ver := globals.Version
if name == "" {
name = "neoirc"
}
if ver == "" {
ver = "dev"
}
return name + "-" + ver
}
// sendIRCError maps a service.IRCError to an IRC numeric // sendIRCError maps a service.IRCError to an IRC numeric
// reply on the wire. // reply on the wire.
func (c *Conn) sendIRCError(err error) { func (c *Conn) sendIRCError(err error) {
@@ -1384,7 +1403,7 @@ func (c *Conn) handleUserhost(
func (c *Conn) handleVersion(ctx context.Context) { func (c *Conn) handleVersion(ctx context.Context) {
_ = ctx _ = ctx
version := "neoirc-0.1" version := versionString()
c.sendNumeric( c.sendNumeric(
irc.RplVersion, irc.RplVersion,
@@ -1426,7 +1445,7 @@ func (c *Conn) handleInfo(ctx context.Context) {
infoLines := []string{ infoLines := []string{
"neoirc — IRC semantics over HTTP", "neoirc — IRC semantics over HTTP",
"Version: neoirc-0.1", "Version: " + versionString(),
"Written in Go", "Written in Go",
} }