diff options
author | Runxi Yu <me@runxiyu.org> | 2025-03-22 01:17:06 +0800 |
---|---|---|
committer | Runxi Yu <me@runxiyu.org> | 2025-03-22 02:02:39 +0800 |
commit | 62045c958b0d58d61e6d22deb88eed7b9ae28a4e (patch) | |
tree | 93ae68d77f6cc21de45a9f4edbdffb6cd0b5a1ae | |
parent | Use MIT because miniirc (diff) | |
download | go-lindenii-irc-62045c958b0d58d61e6d22deb88eed7b9ae28a4e.tar.gz go-lindenii-irc-62045c958b0d58d61e6d22deb88eed7b9ae28a4e.tar.zst go-lindenii-irc-62045c958b0d58d61e6d22deb88eed7b9ae28a4e.zip |
Basic IRCv3 message parser
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | conn.go | 42 | ||||
-rw-r--r-- | errors.go | 6 | ||||
-rw-r--r-- | message.go | 125 | ||||
-rw-r--r-- | source.go | 46 | ||||
-rw-r--r-- | unsafe.go | 11 |
6 files changed, 235 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..d69cd03 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# 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. @@ -0,0 +1,42 @@ +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)) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..84795f5 --- /dev/null +++ b/errors.go @@ -0,0 +1,6 @@ +package irc + +import "errors" + +var ErrInvalidIRCv3Tag = errors.New("invalid ircv3 tag") +var ErrMalformedMsg = errors.New("malformed irc message") diff --git a/message.go b/message.go new file mode 100644 index 0000000..190c198 --- /dev/null +++ b/message.go @@ -0,0 +1,125 @@ +// 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 +} diff --git a/source.go b/source.go new file mode 100644 index 0000000..8fc9848 --- /dev/null +++ b/source.go @@ -0,0 +1,46 @@ +// 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 +} diff --git a/unsafe.go b/unsafe.go new file mode 100644 index 0000000..1c00c7b --- /dev/null +++ b/unsafe.go @@ -0,0 +1,11 @@ +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)) +} |