Commit Graph
12 Commits
Author SHA1 Message Date
sneak c28a8817a4 Resolve IRC client hostnames off the accept loop (closes #156)
check / check (push) Waiting to run
acceptLoop called newConn, which did the reverse DNS lookup, with its
three-second timeout, before the loop could accept the next
connection, so one client whose address had no answering reverse DNS
held up every other IRC connect. serve now does the lookup at its
start, on the connection's own goroutine, and the accept loop only
accepts.

The server keeps the lookup function in a field, as it does the ping
times, so that a test can give it one that blocks. The new test
checks that a second client registers while the first one's lookup
has not returned.

Model: opus-5-5
2026-10-21 01:02:40 +00:00
clawbot 497514d3df Wait for IRC listener cleanup and HTTP shutdown on stop (closes #159)
check / check (push) Waiting to run
Stopping the server returned before the IRC listener's connections had
run their cleanup, and the database closed under them, so their
sessions, clients and channel memberships stayed and their nicks stayed
taken after a restart.

The IRC listener's stop now waits up to five seconds for every
connection's cleanup, which now runs on a context that stopping does
not cancel. The HTTP server's stop hook now runs Shutdown itself and
waits for it; httpServer is set before serving starts, and the server's
own signal watcher, which raced fx's stop hooks, is gone.

Model: opus-5-5
2026-10-20 23:48:30 +02:00
clawbot e3d01454b3 PING a quiet IRC listener client before closing it (closes #155)
check / check (push) Canceled after 0s
serve closed a connection after five minutes with nothing read and never
sent a PING, so a quiet registered client lost its session. It now sends
PING :<server> after pingInterval with nothing read, and closes the
connection when nothing is read within pongDeadline after that. Every
line read starts both waits again. readTimeout is removed.

Unregistered connections are sent the PING too, so a PONG before
registration is now ignored instead of answered with ERR_NOTREGISTERED.

Model: opus-5-5
2026-10-20 20:53:00 +02:00
clawbot 0b09cf838c Check nicks on the IRC listener with the HTTP API's rule (closes #142)
check / check (push) Canceled after 0s
The nick rule moves from internal/handlers to internal/service as
ValidNick, and the HTTP API and both IRC listener NICK paths call it.
The IRC listener used to cut a nick to 32 bytes and accept anything
else, so a nick with spaces such as "bob PRIVMSG #inj :hi" made the
lines built from it read as another user's PRIVMSG. It now answers a
nick the rule refuses with ERR_ERRONEUSNICKNAME (432), before and
after registration, and keeps the nick as it was.

Model: opus-5-5
2026-10-20 17:31:31 +02:00
clawbot 1308769a25 Fix IRC listener relay of bodies and modes, and JOIN names (closes #127)
check / check (push) Canceled after 0s
The IRC listener relayed only the first line of a PRIVMSG or NOTICE
body; it now sends one line per body line. A relayed MODE change now
carries its key, limit or mask, each value its own parameter, also in
the echo to the user who made it. CR, LF and NUL are removed from each
parameter and from each whole line sent to an IRC listener connection,
so no stored value can start a line of its own. The channel name check
moved into service.JoinChannel, so the IRC listener's JOIN refuses the
same names with 403.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
2026-10-20 13:48:16 +02:00
clawbot 7c6f0bc01e Run SQLite in WAL mode with a busy timeout the driver reads (closes #123)
check / check (push) Canceled after 0s
The default database URL asked for WAL mode with `_journal_mode` and
`_busy_timeout`, which the SQLite driver does not read, so the database
ran in rollback-journal mode. The default now uses the driver's
`_pragma=journal_mode(WAL)` and `_pragma=busy_timeout(5000)` parameters,
in one constant that both the config default and the database fallback
use. The database no longer sets its own busy timeout after opening, so
the URL's value applies. A new test opens a file database with those
parameters through db.New and checks the journal mode and busy timeout.
README.md's database text now matches, and an in-memory test database
drops the same ignored parameter.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
2026-10-20 11:30:09 +02:00
clawbot a054bf828d Give each IRC listener test its own database (closes #129)
check / check (push) Canceled after 0s
Each test environment in internal/ircserver named its shared in-memory
database after the test. A repeat run of a test in the same process
could open the database that an earlier run's server was still
holding, and start on that run's users. The database now gets a random
name, and a new test checks that two environments open at once do not
share one.

The database close and the server stop are now registered as soon as
each is opened, so a setup that fails part-way still closes its
database.

Model: opus-5-5
2026-10-08 12:01:48 +02:00
clawbot ccb2bf7960 Keep active IRC listener users from expiring (closes #121)
check / check (push) Canceled after 0s
Every command a registered IRC listener client sends, PING and PONG
included, now sets last_seen on its session and its client, so the
idle cleanup no longer removes an IRC user who is still active; one
who sends nothing for SESSION_IDLE_TIMEOUT is still removed. The two
UPDATEs that GetSessionByToken ran for every authenticated HTTP API
request move into UpdateLastSeen, which both transports now call. The
IRC listener refreshes on every command, with no interval between
writes: that is what every HTTP API request already costs. README.md
describes last_seen to match.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
2026-10-08 10:47:55 +02:00
clawbot 0f2e63d0cc Add the tier 3 utility IRC commands (closes #87)
check / check (push) Canceled after 0s
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
clawbot bb7bcb31ea Re-vendor the canonical files from sneak/prompts at dd4027b (closes #112)
check / check (push) Failing after 4s
The shared files are fetched from sneak/prompts dd4027b, with this
repository's own entries after the shared content in .gitignore,
.editorconfig and .dockerignore.

Lint and tests are Dockerfile phases (golangci-lint v2.14.0, Debian Go
1.24.13) that the build stage depends on, and the Makefile targets call
the script/ entrypoints. make fmt also formats Markdown with prettier.

Fixes for the new lint findings: the auth cookie is always Secure, an
IRC connection's relay goroutine stops when the connection closes, and
repeated strings are constants. neoirc-cli treats a plain-HTTP server on
localhost or a loopback address as secure, so local use keeps its session.

Whether the 60-second test cap covers building the test phase is open
on sneak/prompts issue 113.

Model: opus-5-5
2026-10-06 21:01:46 +02:00
clawbotandclawbot f829f9e3da Change module path to sneak.berlin/go/neoirc (#99)
check / check (push) Failing after 1m39s
Changes the Go module path from `git.eeqj.de/sneak/neoirc` to `sneak.berlin/go/neoirc`.

All occurrences updated:
- `go.mod` module directive
- All Go import paths across 35 `.go` files (107 import statements)
- All JSON schema `$id` URIs across 30 `.json` files in `schema/`

No functional changes — this is a pure rename of the module path.

`docker build .` passes clean (formatting, linting, all tests, binary build).

closes #98

Co-authored-by: clawbot <clawbot@users.noreply.git.eeqj.de>
Reviewed-on: #99
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-04-01 23:05:41 +02:00
0250f14fea feat: add traditional IRC wire protocol listener (closes #89) (#94)
check / check (push) Successful in 5s
## Summary

Adds a backward-compatible IRC wire protocol listener (RFC 1459/2812) that allows standard IRC clients (irssi, weechat, hexchat, etc.) to connect directly via TCP.

## Changes

### New package: `internal/ircserver/`

- **`parser.go`** — IRC wire protocol message parser and formatter
- **`server.go`** — TCP listener with Fx lifecycle integration
- **`conn.go`** — Per-connection handler with registration flow, PING/PONG, welcome burst
- **`commands.go`** — All IRC command handlers (JOIN, PART, PRIVMSG, MODE, TOPIC, KICK, WHOIS, etc.)
- **`relay.go`** — Message relay goroutine that delivers queued messages to IRC clients in wire format

### Modified files

- **`internal/config/config.go`** — Added `IRC_LISTEN_ADDR` environment variable
- **`internal/handlers/handlers.go`** — Broker is now injected via Fx (shared with IRC server)
- **`cmd/neoircd/main.go`** — Registered `broker.New`, `ircserver.New` as Fx providers
- **`pkg/irc/commands.go`** — Added `CmdUser` and `CmdInvite` constants
- **`README.md`** — Added IRC Protocol Listener documentation section

### Tests

- Parser unit tests (table-driven, round-trip verification)
- Integration tests: registration, PING/PONG, JOIN, PART, PRIVMSG (channel + DM), NICK change, duplicate nick rejection, LIST, WHOIS, QUIT, TOPIC, MODE, WHO, LUSERS, MOTD, AWAY, PASS, CAP negotiation, unknown commands, pre-registration errors
- Benchmarks for parser and formatter

## Key Design Decisions

- **Optional**: Listener is only started when `IRC_LISTEN_ADDR` is set
- **Shared infrastructure**: Same DB, broker, and session system as HTTP API
- **Full bridge**: IRC ↔ HTTP messages are interoperable
- **No schema changes**: Reuses existing tables
- **Broker as Fx dependency**: Extracted from handlers to be shared

## Supported Commands

Connection: NICK, USER, PASS, QUIT, PING/PONG, CAP
Channels: JOIN, PART, MODE, TOPIC, NAMES, LIST, KICK, INVITE
Messaging: PRIVMSG, NOTICE
Info: WHO, WHOIS, LUSERS, MOTD, AWAY, USERHOST
Operator: OPER

closes #89

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: sneak/chat#94
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
Co-committed-by: clawbot <sneak+clawbot@sneak.cloud>
2026-04-01 05:00:04 +02:00