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 /message.go | |
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
Diffstat (limited to 'message.go')
-rw-r--r-- | message.go | 125 |
1 files changed, 125 insertions, 0 deletions
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 +} |