Files
zzg 357ef259a4
ci / test (push) Canceled after 0s
initial commit
2026-09-25 22:07:44 -07:00

96 lines
3.2 KiB
Markdown

# irc
An application used to communicate via IRC networks: a client library plus a
terminal client.
Implements RFC 1459/2812 message parsing, IRCv3 message tags, TLS,
automatic reconnection with exponential backoff, channel state tracking and
CTCP.
## Install
```sh
go install github.com/zg/irc/cmd/irc@latest
```
## Use
```sh
irc -nick yournick -channels '#go,#general'
irc -nick yournick -server irc.example.com:6667 -tls=false
irc -nick yournick -v # log every protocol message
```
Commands: `/join`, `/part`, `/msg`, `/me`, `/nick`, `/topic`, `/names`,
`/switch`, `/raw`, `/quit`. Anything not starting with `/` is sent to the
current channel.
## Library
```go
client := irc.NewClient(irc.Config{
Server: "irc.libera.chat:6697",
Nick: "mybot",
TLS: true,
Channels: []string{"#mychannel"},
Reconnect: true,
})
client.Handle(irc.CmdPrivmsg, func(c *irc.Client, m *irc.Message) {
if m.Trailing() == "!ping" {
c.Privmsg(m.Param(0), "pong")
}
})
client.Run(ctx)
```
A handler registered for `"*"` sees every message, which is what a logger
wants.
## Design notes
**A blocked socket read ignores context cancellation.** `Run` looked like it
respected its context — there is a `select` in the read loop — but
`scanner.Scan()` blocks on the socket and only returns when a line arrives.
Cancelling therefore did nothing until the server next spoke. The connection
is now closed when the context is done, which is the only way to unblock the
read. A test asserts `Run` returns within three seconds of cancellation.
**Silence is probed, not trusted.** A TCP connection can stay open long
after the peer has gone. The client PINGs after a quiet interval and drops
the connection when there has been no traffic for `PingTimeout`, which then
triggers reconnection.
**PING is answered before user handlers run.** Failing to reply gets the
client disconnected, so it cannot be left to a handler that might block or
be absent.
**Newlines are stripped from everything sent.** Without that, a message body
containing `\r\n` would inject an arbitrary second command — so
`PRIVMSG #c :text\r\nQUIT` would really quit. There is a test for exactly
that. Lines are also truncated to the 512-byte protocol limit at a rune
boundary, rather than letting the server cut mid-character.
**A malformed line never kills the connection.** Servers do emit junk; the
read loop skips what it cannot parse and carries on. Tested by feeding the
client garbage and checking that the next valid message is still delivered.
**Nick collisions retry automatically** by appending an underscore, and the
nick actually in use is taken from the welcome numeric rather than assumed —
the server may not give you what you asked for.
**Channel state follows JOIN, NAMES, PART, KICK, QUIT and NICK.** A nick
change renames the user in every channel rather than leaving a stale entry,
and mode prefixes (`@`, `+`) are stripped from NAMES replies.
## Tests
The client is tested against a real mock IRC server over a loopback socket,
not a stubbed connection — registration, PING/PONG, state tracking and
reconnection only work end to end if the socket handling does.
```sh
go test -race ./...
```