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

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

go install github.com/zg/irc/cmd/irc@latest

Use

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

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.

go test -race ./...
S
Description
No description provided
Readme
35 KiB
Languages
Go 100%