]> git.openfl.eu Git - twitch-chat.git/commitdiff
parse and format tags
authorFl_GUI <flor.guilini@hotmail.com>
Sun, 12 May 2024 15:18:01 +0000 (17:18 +0200)
committerFl_GUI <flor.guilini@hotmail.com>
Sun, 12 May 2024 15:18:01 +0000 (17:18 +0200)
irc/message.go
irc/message_test.go
irc/parse.go
irc/parse_test.go
irc/string.go
twitch/core/capabilities.go
twitch/core/messages/auth.go
twitch/core/messages/capability.go
twitch/core/messages/join.go
twitch/core/messages/ping.go
x/corechatclient/main.go

index 7e20f29724ea46e7a85780e2169a314d26bcfaa6..fcf52ad0cb63b117cfd1a647ae45603eef0c24a3 100644 (file)
@@ -1,11 +1,19 @@
 package irc
 
 type Message struct {
+       Tags
        Prefix
        Command
        Params
 }
 
+type Tags []Tag
+
+type Tag struct {
+       ClientOnly         bool
+       Vendor, Key, Value string
+}
+
 type Prefix struct {
        Name, User, Host string
 }
index dce60518605c707b7a01c0f30b0b3b4468d0f26c..97d06c76068fc9227fd411e1bd23160608958a0e 100644 (file)
@@ -10,32 +10,44 @@ func TestToString(t *testing.T) {
                in       Message
                expected string
        }{
-               {Message{Prefix{}, "foo", Params{[]string{}, ""}}, "foo"},
-               {Message{Prefix{}, "foo", Params{[]string{"bar"}, ""}}, "foo bar"},
-               {Message{Prefix{"host", "", ""}, "foo", Params{[]string{"bar"}, ""}}, ":host foo bar"},
-               {Message{Prefix{"host", "user", ""}, "foo", Params{[]string{"bar"}, ""}}, ":host!user foo bar"},
-               {Message{Prefix{"host", "user", "host"}, "foo", Params{[]string{"bar"}, ""}}, ":host!user@host foo bar"},
-               {Message{Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, ""}}, ":host@host foo bar"},
-               {Message{Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}}, ":host@host foo bar :spam"},
+               {Message{Tags{}, Prefix{}, "foo", Params{[]string{}, ""}}, "foo"},
+               {Message{Tags{}, Prefix{}, "foo", Params{[]string{"bar"}, ""}}, "foo bar"},
+               {Message{Tags{}, Prefix{"host", "", ""}, "foo", Params{[]string{"bar"}, ""}}, ":host foo bar"},
+               {Message{Tags{}, Prefix{"host", "user", ""}, "foo", Params{[]string{"bar"}, ""}}, ":host!user foo bar"},
+               {Message{Tags{}, Prefix{"host", "user", "host"}, "foo", Params{[]string{"bar"}, ""}}, ":host!user@host foo bar"},
+               {Message{Tags{}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, ""}}, ":host@host foo bar"},
+               {Message{Tags{}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}}, ":host@host foo bar :spam"},
+               {Message{Tags{Tag{false, "", "key", ""}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}}, "@key :host@host foo bar :spam"},
+               {Message{Tags{Tag{false, "", "key", "value"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}}, "@key=value :host@host foo bar :spam"},
+               {Message{Tags{Tag{false, "", "key", "foo bar"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}}, "@key=foo\\sbar :host@host foo bar :spam"},
+               {Message{Tags{Tag{false, "vend", "key", "foo bar"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}}, "@vend/key=foo\\sbar :host@host foo bar :spam"},
+               {Message{Tags{Tag{true, "vend", "key", "foo bar"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}}, "@+vend/key=foo\\sbar :host@host foo bar :spam"},
+               {Message{Tags{Tag{false, "", "pos", "one"}, Tag{false, "", "pos", "two"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}}, "@pos=one;pos=two :host@host foo bar :spam"},
        }
 
        for _, d := range data {
                out := d.in.String()
                if out != d.expected {
-                       t.Errorf("%#v.ToString() = %#v but expected %#v\n", d.in, out, d.expected)
+                       t.Errorf("%#v.ToString() = \n%#v\n but expected \n%#v\n", d.in, out, d.expected)
                }
        }
 }
 
 func TestParseAfterString(t *testing.T) {
        data := []Message{
-               Message{Prefix{}, "foo", Params{}},
-               Message{Prefix{}, "foo", Params{[]string{"bar"}, ""}},
-               Message{Prefix{"host", "", ""}, "foo", Params{[]string{"bar"}, ""}},
-               Message{Prefix{"host", "user", ""}, "foo", Params{[]string{"bar"}, ""}},
-               Message{Prefix{"host", "user", "host"}, "foo", Params{[]string{"bar"}, ""}},
-               Message{Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, ""}},
-               Message{Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}},
+               Message{Tags{}, Prefix{}, "foo", Params{}},
+               Message{Tags{}, Prefix{}, "foo", Params{[]string{"bar"}, ""}},
+               Message{Tags{}, Prefix{"host", "", ""}, "foo", Params{[]string{"bar"}, ""}},
+               Message{Tags{}, Prefix{"host", "user", ""}, "foo", Params{[]string{"bar"}, ""}},
+               Message{Tags{}, Prefix{"host", "user", "host"}, "foo", Params{[]string{"bar"}, ""}},
+               Message{Tags{}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, ""}},
+               Message{Tags{}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}},
+               Message{Tags{Tag{false, "", "key", ""}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}},
+               Message{Tags{Tag{false, "", "key", "value"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}},
+               Message{Tags{Tag{false, "", "key", "foo bar"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}},
+               Message{Tags{Tag{false, "vend", "key", "foo bar"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}},
+               Message{Tags{Tag{true, "vend", "key", "foo bar"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}},
+               Message{Tags{Tag{false, "", "pos", "one"}, Tag{false, "", "pos", "two"}}, Prefix{"host", "", "host"}, "foo", Params{[]string{"bar"}, "spam"}},
        }
 
        for _, d := range data {
index cf64312f4ffe9d653c1f87b2b76bdd7f973e834d..0d64dc43025a9b4090458422329a6a7597a52383 100644 (file)
@@ -8,6 +8,8 @@ import "strings"
 //
 // This function does not do any error handling (yet) of malformed input
 func ParseBytes(data []byte) (Message, error) {
+       var tag = Tag{}
+       var tags = []Tag{}
        var name, user, host, command string
        var params []string
        var trailing string
@@ -16,6 +18,8 @@ func ParseBytes(data []byte) (Message, error) {
        // states
        const (
                s_init int = iota
+               s_tagKey
+               s_tagValue
                s_servername
                s_user
                s_host
@@ -31,6 +35,7 @@ func ParseBytes(data []byte) (Message, error) {
        }
 
        var skipSpace = false
+       var unescape = false
        state := s_init
        for _, c := range data {
                if c == ' ' && skipSpace {
@@ -42,13 +47,65 @@ func ParseBytes(data []byte) (Message, error) {
                switch state {
 
                case s_init:
-                       if c == ':' {
+                       if c == '@' {
+                               state = s_tagKey
+                       } else if c == ':' {
                                state = s_servername
                        } else {
                                state = s_command
                                b.WriteByte(c)
                        }
 
+               case s_tagKey:
+                       if c == '+' {
+                               tag.ClientOnly = true
+                       } else if c == '/' {
+                               tag.Vendor = flush()
+                       } else if c == '=' {
+                               tag.Key = flush()
+                               state = s_tagValue
+                       } else if c == ' ' {
+                               tag.Key = flush()
+                               state = s_init
+                               tags = append(tags, tag)
+                       } else {
+                               b.WriteByte(c)
+                       }
+
+               case s_tagValue:
+                       if c == ' ' {
+                               tag.Value = flush()
+                               state = s_init
+                               tags = append(tags, tag)
+                       } else if c == ';' {
+                               tag.Value = flush()
+                               state = s_tagKey
+                               tags = append(tags, tag)
+                               tag = Tag{}
+                       } else if c == '\\' && !unescape {
+                               unescape = true
+                       } else {
+                               if unescape {
+                                       switch c {
+                                       case ':':
+                                               b.WriteByte(';')
+                                       case 's':
+                                               b.WriteByte(' ')
+                                       case '\\':
+                                               b.WriteByte('\\')
+                                       case 'r':
+                                               b.WriteByte('\r')
+                                       case 'n':
+                                               b.WriteByte('\n')
+                                       default:
+                                               b.WriteByte(c)
+                                       }
+                                       unescape = false
+                               } else {
+                                       b.WriteByte(c)
+                               }
+                       }
+
                case s_servername:
                        if c == '!' {
                                state = s_user
@@ -130,6 +187,7 @@ func ParseBytes(data []byte) (Message, error) {
        }
 
        res := Message{
+               tags,
                Prefix{name, user, host},
                Command(command),
                Params{
index e38810d747ba8350df2690c3311f93f905f6fd52..7fe8c79be14f398be6cc642f9529b4f8850284fd 100644 (file)
@@ -10,16 +10,18 @@ func TestCorrectParsing(t *testing.T) {
                in       string
                expected Message
        }{
-               {":server!user@host command param1 param2 param3 :trailing", Message{Prefix{"server", "user", "host"}, Command("command"), Params{[]string{"param1", "param2", "param3"}, "trailing"}}},
-               {":server!user@host   command   param1   param2   param3   :trailing space", Message{Prefix{"server", "user", "host"}, Command("command"), Params{[]string{"param1", "param2", "param3"}, "trailing space"}}},
-               {"foo", Message{Prefix{}, Command("foo"), Params{}}},
-               {"foo bar spam", Message{Prefix{}, Command("foo"), Params{[]string{"bar", "spam"}, ""}}},
-               {"foo bar :spam", Message{Prefix{}, Command("foo"), Params{[]string{"bar"}, "spam"}}},
-               {"foo bar :", Message{Prefix{}, Command("foo"), Params{[]string{"bar"}, ""}}},
-               {":server foo", Message{Prefix{"server", "", ""}, Command("foo"), Params{}}},
-               {":server!user foo", Message{Prefix{"server", "user", ""}, Command("foo"), Params{}}},
-               {":server@host foo", Message{Prefix{"server", "", "host"}, Command("foo"), Params{}}},
-               {":server@host   foo", Message{Prefix{"server", "", "host"}, Command("foo"), Params{}}},
+               {":server!user@host command param1 param2 param3 :trailing", Message{Tags{}, Prefix{"server", "user", "host"}, Command("command"), Params{[]string{"param1", "param2", "param3"}, "trailing"}}},
+               {":server!user@host   command   param1   param2   param3   :trailing space", Message{Tags{}, Prefix{"server", "user", "host"}, Command("command"), Params{[]string{"param1", "param2", "param3"}, "trailing space"}}},
+               {"foo", Message{Tags{}, Prefix{}, Command("foo"), Params{}}},
+               {"foo bar spam", Message{Tags{}, Prefix{}, Command("foo"), Params{[]string{"bar", "spam"}, ""}}},
+               {"foo bar :spam", Message{Tags{}, Prefix{}, Command("foo"), Params{[]string{"bar"}, "spam"}}},
+               {"foo bar :", Message{Tags{}, Prefix{}, Command("foo"), Params{[]string{"bar"}, ""}}},
+               {":server foo", Message{Tags{}, Prefix{"server", "", ""}, Command("foo"), Params{}}},
+               {":server!user foo", Message{Tags{}, Prefix{"server", "user", ""}, Command("foo"), Params{}}},
+               {":server@host foo", Message{Tags{}, Prefix{"server", "", "host"}, Command("foo"), Params{}}},
+               {":server@host   foo", Message{Tags{}, Prefix{"server", "", "host"}, Command("foo"), Params{}}},
+               {"@key=val foo", Message{Tags{Tag{false, "", "key", "val"}}, Prefix{"", "", ""}, Command("foo"), Params{}}},
+               {"@key=val;key=b\\sa\\sr foo", Message{Tags{Tag{false, "", "key", "val"}, Tag{false, "", "key", "b a r"}}, Prefix{"", "", ""}, Command("foo"), Params{}}},
        }
 
        for _, d := range data {
@@ -40,6 +42,7 @@ func TestStringAfterParse(t *testing.T) {
                ":server@host foo",
                ":server@host foo :trailing",
                ":server@host foo spam bar :trailing",
+               "@+foo/bar=baz;escape=all\\sthe\\nthings\\rno\\:matter\\\\what :server@host foo spam bar :trailing",
                // String ∘ Parsing is not a bijection because of whitespace
                //":server@host foo spam bar :",
                //":server!user@host   command   param1   param2   param3   :trailing space",
index 9482c0766a0b0e9698d8d5b1963811687e8fa459..5e73cec77932ef15bac10abed27b59e327cbc5ff 100644 (file)
@@ -30,6 +30,52 @@ func (c Message) WriteTo(w io.Writer) (n int, err error) {
                }
        }
 
+       writeEscaped := func(s string) {
+               if err == nil {
+                       for _, c := range s {
+                               switch c {
+                               case ';':
+                                       write("\\:")
+                               case ' ':
+                                       write("\\s")
+                               case '\\':
+                                       write("\\\\")
+                               case '\r':
+                                       write("\\r")
+                               case '\n':
+                                       write("\\n")
+                               default:
+                                       m, err = buff.WriteRune(c)
+                                       n += m
+                               }
+                       }
+               }
+       }
+
+       // tags
+       for i, tag := range c.Tags {
+               if i == 0 {
+                       write("@")
+               } else {
+                       write(";")
+               }
+               if tag.ClientOnly {
+                       write("+")
+               }
+               if tag.Vendor != "" {
+                       write(tag.Vendor)
+                       write("/")
+               }
+               write(tag.Key)
+               if tag.Value != "" {
+                       write("=")
+                       writeEscaped(tag.Value)
+               }
+       }
+       if len(c.Tags) != 0 {
+               write(" ")
+       }
+
        // prefix
        if c.Prefix.Name != "" {
                write(":")
index e80ca60ecc894296081548981429125a91def982..4d9b6b5666ca68115b57407e4156ca03ca068755 100644 (file)
@@ -1,7 +1,6 @@
 package core
 
 import (
-       "errors"
        "fmt"
        "strings"
        "twitchchat/twitch/core/messages"
@@ -18,10 +17,6 @@ const (
 // See https://dev.twitch.tv/docs/irc/capabilities/
 func (c *Conn) WithCapability(msgs <-chan messages.Message, capabilities ...string) (<-chan messages.Message, error) {
        caps := strings.Join(capabilities, " ")
-       if strings.Contains(caps, TagsCapability) {
-               return msgs, fmt.Errorf("TagsCapability is unavailable: %v", errors.ErrUnsupported)
-       }
-
        res := make(chan messages.Message)
        capResp := make(chan messages.Message, 2)
 
index 6993819fce13853ffa5a89ea5068092f831a1707..775f93818bcc4be9d383ac6fdd0378231f47329f 100644 (file)
@@ -8,6 +8,7 @@ import (
 
 func Pass(nickname string) Message {
        return Message{
+               irc.Tags{},
                irc.Prefix{},
                irc.Command(commands.Pass),
                irc.Params{
@@ -19,6 +20,7 @@ func Pass(nickname string) Message {
 
 func Nick(nickname string) Message {
        return Message{
+               irc.Tags{},
                irc.Prefix{},
                irc.Command(commands.Nick),
                irc.Params{
index cff9ce81ad6a619c910e9a73b4a3d585f9b5b4d3..7898fff7f342955dba8714eaecaefaee8c537ada 100644 (file)
@@ -7,6 +7,7 @@ import (
 
 func Capability(capability string) Message {
        return Message{
+               irc.Tags{},
                irc.Prefix{},
                irc.Command(commands.Cap),
                irc.Params{
index 0d5cc323d40f48616350a914fdd906083e024605..a5c6439ac01785793891eadb725c34f7d891b35f 100644 (file)
@@ -16,6 +16,7 @@ func Join(channels ...string) Message {
                b.WriteString(c)
        }
        return Message{
+               irc.Tags{},
                irc.Prefix{},
                irc.Command(commands.Join),
                irc.Params{
index 0e5d96301851324c9877b8a4c3d6e515b795aed6..b48bc1371762c5174dd0b98bf4c94764a9014d6f 100644 (file)
@@ -15,6 +15,7 @@ func (p Message) Text() string {
 
 func Ping(s string) Message {
        return Message{
+               irc.Tags{},
                irc.Prefix{},
                irc.Command(commands.Ping),
                irc.Params{[]string{s}, ""},
@@ -23,6 +24,7 @@ func Ping(s string) Message {
 
 func (p Message) ToPong() Message {
        return Message{
+               irc.Tags{},
                irc.Prefix{},
                irc.Command(commands.Pong),
                irc.Params{[]string{p.Text()}, ""},
index 8f7dffdd1a5399d2611caa60fcfc3c421fe7e5c4..1bb5f34c6d8006faf9dc997e762d498d7163a902 100644 (file)
@@ -33,7 +33,7 @@ func main() {
 
        msgs = conn.PingPong(msgs)
 
-       if msgs, err = conn.WithCapability(msgs, core.MembershipCapability); err != nil {
+       if msgs, err = conn.WithCapability(msgs, core.TagsCapability, core.MembershipCapability); err != nil {
                if errors.Is(err, core.UnsupportedCapabilitiesError) {
                        fmt.Printf("%#v\n", err.(core.UnsupportedCapabilities))
                } else {