aboutsummaryrefslogtreecommitdiff
path: root/message.go
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-03-22 01:17:06 +0800
committerRunxi Yu <me@runxiyu.org>2025-03-22 02:02:39 +0800
commit62045c958b0d58d61e6d22deb88eed7b9ae28a4e (patch)
tree93ae68d77f6cc21de45a9f4edbdffb6cd0b5a1ae /message.go
parentUse MIT because miniirc (diff)
downloadgo-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.go125
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
+}