Lindenii Project Forge
Basic IRCv3 message parser
# IRC client library This was partially inspired by [miniirc](https://github.com/luk3yx/miniirc) but aims to be a little bit simpler. All message handling and state tracking are left to the user.
package irc import ( "bufio" "net" "slices" ) type Conn struct { netConn net.Conn bufReader *bufio.Reader } func NewConn(netConn net.Conn) Conn { return Conn{ netConn: netConn, bufReader: bufio.NewReader(netConn), } } func (c *Conn) ReadMessage() (msg Message, err error) { raw, err := c.bufReader.ReadSlice('\n') if err != nil { return } if raw[len(raw) - 1] == '\r' { raw = raw[:len(raw) - 1] } msg, err = Parse(slices.Clone(raw)) return } func (c *Conn) Write(p []byte) (n int, err error) { return c.netConn.Write(p) } func (c *Conn) WriteString(s string) (n int, err error) { return c.netConn.Write(stringToBytes(s)) }
package irc import "errors" var ErrInvalidIRCv3Tag = errors.New("invalid ircv3 tag") var ErrMalformedMsg = errors.New("malformed irc message")
// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Copyright (c) 2018-2024 luk3yx <https://luk3yx.github.io> // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package irc import ( "bytes" ) type Message struct { Command string Source *Source Tags map[string]string Args []string } // All strings returned are borrowed from the input byte slice. func Parse(raw []byte) (msg Message, err error) { sp := bytes.Split(raw, []byte{' '}) // TODO: Use bytes.Cut instead here if bytes.HasPrefix(sp[0], []byte{'@'}) { // TODO: Check size manually if len(sp[0]) < 2 { err = ErrMalformedMsg return } sp[0] = sp[0][1:] msg.Tags, err = tagsToMap(sp[0]) if err != nil { return } if len(sp) < 2 { err = ErrMalformedMsg return } sp = sp[1:] } else { msg.Tags = nil // TODO: Is a nil map the correct thing to use here? } if bytes.HasPrefix(sp[0], []byte{':'}) { // TODO: Check size manually if len(sp[0]) < 2 { err = ErrMalformedMsg return } sp[0] = sp[0][1:] source := parseSource(sp[0]) msg.Source = &source if len(sp) < 2 { err = ErrMalformedMsg return } sp = sp[1:] } msg.Command = bytesToString(sp[0]) if len(sp) < 2 { return } sp = sp[1:] for i := 0; i < len(sp); i++ { if len(sp[i]) == 0 { continue } if sp[i][0] == ':' { if len(sp[i]) < 2 { sp[i] = []byte{} } else { sp[i] = sp[i][1:] } msg.Args = append(msg.Args, bytesToString(bytes.Join(sp[i:], []byte{' '}))) // TODO: Avoid Join by not using sp in the first place break } msg.Args = append(msg.Args, bytesToString(sp[i])) } return } var ircv3TagEscapes = map[byte]byte{ ':': ';', 's': ' ', 'r': '\r', 'n': '\n', } func tagsToMap(raw []byte) (tags map[string]string, err error) { tags = make(map[string]string) for rawTag := range bytes.SplitSeq(raw, []byte{';'}) { key, value, found := bytes.Cut(rawTag, []byte{'='}) if !found { err = ErrInvalidIRCv3Tag return } if len(value) == 0 { tags[bytesToString(key)] = "" } else { if !bytes.Contains(value, []byte{'\\'}) { tags[bytesToString(key)] = bytesToString(value) } else { valueUnescaped := bytes.NewBuffer(make([]byte, 0, len(value))) for i := 0; i < len(value); i++ { if value[i] == '\\' { i++ byteUnescaped, ok := ircv3TagEscapes[value[i]] if !ok { byteUnescaped = value[i] } valueUnescaped.WriteByte(byteUnescaped) } else { valueUnescaped.WriteByte(value[i]) } } tags[bytesToString(key)] = bytesToString(valueUnescaped.Bytes()) } } } return }
// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package irc import "bytes" type Source interface { AsSourceString() string } func parseSource(s []byte) Source { nick, userhost, found := bytes.Cut(s, []byte{'!'}) if !found { return Server{name: bytesToString(s)} } user, host, found := bytes.Cut(userhost, []byte{'@'}) if !found { return Server{name: bytesToString(s)} } return Client{ Nick: bytesToString(nick), User: bytesToString(user), Host: bytesToString(host), } } type Server struct { name string } func (s Server) AsSourceString() string { return s.name } type Client struct { Nick string User string Host string } func (c Client) AsSourceString() string { return c.Nick + "!" + c.User + "@" + c.Host }
package irc import "unsafe" func bytesToString(bytes []byte) (s string) { return unsafe.String(unsafe.SliceData(bytes), len(bytes)) } func stringToBytes(s string) (bytes []byte) { return unsafe.Slice(unsafe.StringData(s), len(s)) }