96 lines
3.2 KiB
Markdown
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 ./...
|
|
```
|