feat: implement Tier 3 utility IRC commands (USERHOST, VERSION, ADMIN, INFO, TIME, KILL, WALLOPS) (closes #87) #96

Open
clawbot wants to merge 13 commits from feature/87-tier3-utility-commands into next
13 Commits
Author SHA1 Message Date
user edbbdc9ca5 style: satisfy golangci-lint on the round-6 changes
check / check (push) Successful in 7m26s
nolintlint: gosec was not firing on the deferred Close.
nonamedreturns + varnamelen in collapseUserModeOps.
2026-09-04 05:52:26 +00:00
user dbdef00e91 test: cover user-mode apply atomicity and non-blocking KILL
check / check (push) Failing after 16s
db: TestSetSessionUserModesIsAtomic installs a trigger that rejects the
is_oper UPDATE after the is_wallops UPDATE has run, proving the '+w-o'
partial-failure the old independent-UPDATE loop exhibited is gone.

ircserver: TestDisconnectDoesNotBlockOnUnresponsiveVictim drives
Disconnect against a net.Pipe victim that never reads and requires the
call to return promptly; verified to fail against the synchronous
implementation. Its counterpart asserts the notification is still
delivered and the socket still closed.
2026-09-04 05:36:16 +00:00
user 209b0ff364 test(ircserver): cover VERSION/ADMIN/TIME under the default empty SERVER_NAME
check / check (push) Failing after 1m16s
Both wire test environments hardcoded ServerName: "test.irc", so no test
exercised the shipped default and the empty server-name parameter went
unnoticed for five rework rounds. Parameterize the env's server name and
add a wire test that runs with it empty, asserting the numerics name
"neoirc" and contain no empty parameter. Verified to fail against the
pre-fix handlers.
2026-09-04 05:34:33 +00:00
user cfff726054 fix(ircserver): KILL notify-and-close must not block the killer
check / check (push) Failing after 24s
WIP: Disconnect ran two blocking writes to the victim's socket on the
killer's goroutine with the full 30s writeTimeout each, so a victim that
stopped reading stalled the killer up to ~60s -- wedging the operator's
serve() loop or the HTTP KILL request. Move the notify-and-close to its
own goroutine and bound both writes with a short killWriteWindow.
2026-09-04 05:33:07 +00:00
user 534d10d719 fix(service): make ApplyUserMode apply stage transactional
check / check (push) Has been cancelled
WIP: the apply loop issued independent UPDATEs, so a failure partway
through '+w-o' left '+w' persisted while the caller reported total
failure, contradicting the doc comment. Collapse the parsed ops to the
final value of each flag and write them in one transaction via the new
db.SetSessionUserModes.
2026-09-04 05:32:19 +00:00
user 86813b506b fix(ircserver): VERSION/ADMIN/TIME use serverSfx fallback
check / check (push) Has been cancelled
WIP: c.cfg.ServerName defaults to "", so the three new wire handlers
emitted an empty server-name parameter under the shipped default config.
c.serverSfx already carries the same "neoirc" fallback the HTTP path
uses, and sendNumeric uses it for the prefix. Empty-ServerName test
follows.
2026-09-04 05:31:29 +00:00
clawbot c20ad88dfe fix: KILL actually disconnects the victim; unify HTTP/IRC divergences
check / check (push) Successful in 1m12s
Addresses the 2026-08-10 FAIL review (findings 1-7, 9, 10).

KILL never terminated the victim's connection on either transport: both
paths called BroadcastQuit, which deletes the session row and tells the
victim's peers it quit, but leaves the victim holding a socket that looks
alive and silently delivers nothing while its nick is freed for reuse.
Service now owns a session-ID keyed registry of live wire connections that
ircserver populates at registration, and both KILL paths go through the new
Service.KillSession, which broadcasts the QUIT and then sends the victim a
KILL and ERROR :Closing Link before closing its socket. The victim's relay
goroutine is cancelled and its cleanup no longer re-broadcasts a QUIT for
an already-deleted session. TestIntegrationKill now asserts the victim
reads to EOF and is gone from NAMES and WHO, not just that an observer saw
the QUIT relay.

HTTP MODE <othernick> with no body answered with the requester's own modes,
because the target check sat inside the mode-change branch. The check is
hoisted above the query/change split, and both transports now compare nicks
with EqualFold since IRC nicks are case-insensitive.

Service.QueryUserMode returned "+" for a database failure, making an
unreadable mode indistinguishable from an unset one; it now returns an
error, and both callers surface it. db.GetUserhostInfo likewise treated
every scan error as "nick not found"; only sql.ErrNoRows is skipped now.

The four new unsynchronized c.nick reads this branch introduced are read
through currentNick() under c.mu, and c.closed is now guarded everywhere
because KILL writes it from another client's goroutine. Conn.send takes a
write mutex, as a connection is now written to by three goroutines.

server.Server.Run was left with no in-tree callers when its body was
inlined into the fx OnStart hook; it is deleted rather than left to drift.

INFO and VERSION had two implementations that had already diverged: the
version string is now Service.ServerVersion and the INFO body is
Service.InfoLines, used verbatim by both transports. The ctx parameters on
handleVersion/handleAdmin/handleInfo/handleTime existed only to be
discarded and are gone.
2026-09-03 15:19:50 +00:00
clawbot f24e33a310 fix: resolve 43 lint findings blocking CI
check / check (push) Successful in 56s
- server.go: drop unused (*Server).serve int return (unparam) and
  remove the dead exitCode field so cleanShutdown no longer writes
  to a field nothing reads.
- service.go: rename range var ch -> modeChar in parseUserModeString
  and the isKnownUserModeChar parameter (varnamelen).
- service_test.go: rename tc -> testCase (varnamelen); lift the
  inline struct and caseState to package-level named types
  (applyUserModeCase, applyUserModeCaseState) with every field
  set explicitly (exhaustruct); split the 167-line case table into
  four categorised helpers (funlen); extract the per-case runner
  and outcome/state verifiers into helpers so TestApplyUserMode
  drops below gocognit 30 and flattens the wantErr nestif block.

No changes to .golangci.yml, Makefile, Dockerfile, or CI config.
No //nolint was used to silence any of these findings.

docker build --no-cache . passes clean: 0 lint issues, all tests
pass with -race, binary compiles.
2026-04-17 14:37:14 +00:00
clawbot 93611dad67 fix: rigorous atomic user mode parser and fix router race in server
check / check (push) Failing after 23s
Mode parser (internal/service/service.go):
- Reject strings without leading + or - (e.g. "xw", "w", "") with
  ERR_UMODEUNKNOWNFLAG instead of silently treating them as "-".
- Support multi-sign transitions: +w-o, -w+o, +o-w+w, -x+y, +y-x. The
  active sign flips each time + or - is seen; subsequent letters apply
  with the active sign.
- Atomic from caller's perspective: parse the whole string to a list of
  ops first, reject the whole request on any unknown mode char, and only
  then apply ops to the DB. Partial application of +w before rejecting
  +o is gone.
- HTTP and IRC still share the same ApplyUserMode entry point.

Router race (internal/server/server.go):
- The fx OnStart hook previously spawned serve() in a goroutine that
  called SetupRoutes asynchronously, while ServeHTTP delegated to
  srv.router. Test harnesses (httptest wrapping srv as Handler) raced
  against SetupRoutes writing srv.router vs ServeHTTP reading it,
  producing the race detector failures in CI on main.
- SetupRoutes is now called synchronously inside OnStart before the
  serve goroutine starts, so srv.router is fully initialized before any
  request can reach ServeHTTP.

Tests (internal/service/service_test.go):
- Replaced the per-mode tests with a single table-driven TestApplyUserMode
  that asserts both the returned mode string and the persisted DB state
  (oper/wallops) for each case, including the malformed and multi-sign
  cases above. The +wz case seeds wallops=true to prove the whole string
  is rejected and +w is not partially applied.
2026-04-17 10:46:24 +00:00
clawbot abe0cc2c30 refactor: unify user mode processing into shared service layer
check / check (push) Failing after 2m28s
Both the HTTP API and IRC wire protocol handlers now call
service.ApplyUserMode/service.QueryUserMode for all user
mode operations. The service layer iterates mode strings
character by character (the correct IRC approach), ensuring
identical behavior regardless of transport.

Removed duplicate mode logic from internal/handlers/utility.go
(buildUserModeString, applyUserModeChange, applyModeChar) and
internal/ircserver/commands.go (buildUmodeString, inline iteration).

Added service-level tests for QueryUserMode, ApplyUserMode
(single-char, multi-char, invalid input, de-oper, +o rejection).
2026-04-02 06:48:55 -07:00
clawbot 327ff37059 fix: address review findings — dynamic version, deduplicate KILL, update README
check / check (push) Successful in 1m3s
2026-04-01 14:44:27 -07:00
clawbot 17479c4f44 fix: rebase onto main, add IRC wire handlers and integration tests for Tier 3 commands
check / check (push) Failing after 2m10s
Rebase onto main to resolve conflicts from module path rename
(sneak.berlin/go/neoirc) and integration test addition.

- Update import paths in utility.go to new module path
- Add IRC wire protocol handlers for VERSION, ADMIN, INFO,
  TIME, KILL, and WALLOPS to ircserver/commands.go
- Register all 6 new commands in the IRC command dispatch map
- Implement proper user MODE +w/-w support for WALLOPS
- Add WALLOPS relay delivery in relay.go
- Add integration tests for all 7 Tier 3 commands:
  USERHOST, VERSION, ADMIN, INFO, TIME, KILL, WALLOPS
- Add newTestEnvWithOper helper for oper-dependent tests
2026-04-01 14:16:09 -07:00
user 9c4ec966fb feat: implement Tier 3 utility IRC commands
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
2026-04-01 14:07:22 -07:00