]> git.openfl.eu Git - twitch-chat.git/commitdiff
chat test and irc parser
authorFl_GUI <flor.guilini@hotmail.com>
Thu, 9 May 2024 13:21:46 +0000 (15:21 +0200)
committerFl_GUI <flor.guilini@hotmail.com>
Thu, 9 May 2024 13:21:46 +0000 (15:21 +0200)
go.mod [new file with mode: 0644]
go.sum [new file with mode: 0644]
irc/conn.go [new file with mode: 0644]
irc/doc.go [new file with mode: 0644]
irc/message.go [new file with mode: 0644]
irc/message_test.go [new file with mode: 0644]
irc/parse.go [new file with mode: 0644]
irc/parse_test.go [new file with mode: 0644]
irc/string.go [new file with mode: 0644]
x/clichat/chat.go [new file with mode: 0644]

diff --git a/go.mod b/go.mod
new file mode 100644 (file)
index 0000000..667ca6e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module twitchchat
+
+go 1.22.3
+
+require golang.org/x/net v0.25.0 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644 (file)
index 0000000..ec14cb3
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
diff --git a/irc/conn.go b/irc/conn.go
new file mode 100644 (file)
index 0000000..8694d0b
--- /dev/null
@@ -0,0 +1,8 @@
+package irc
+
+import "golang.org/x/net/websocket"
+
+// Dial opens a new client connection to a WebSocket.
+func Dial(url_, origin string) (ws *websocket.Conn, err error) {
+       return websocket.Dial(url_, "", origin)
+}
diff --git a/irc/doc.go b/irc/doc.go
new file mode 100644 (file)
index 0000000..08e3f79
--- /dev/null
@@ -0,0 +1,3 @@
+// The Internet Relay Chat connection to twitch is implemented through a websocket
+// This package provides merely parsing over irc messages from a websocket
+package irc
diff --git a/irc/message.go b/irc/message.go
new file mode 100644 (file)
index 0000000..7e20f29
--- /dev/null
@@ -0,0 +1,21 @@
+package irc
+
+type Message struct {
+       Prefix
+       Command
+       Params
+}
+
+type Prefix struct {
+       Name, User, Host string
+}
+
+// a string of letters
+// OR
+// a string of three numbers
+type Command string
+
+type Params struct {
+       Params   []string
+       Trailing string
+}
diff --git a/irc/message_test.go b/irc/message_test.go
new file mode 100644 (file)
index 0000000..dce6051
--- /dev/null
@@ -0,0 +1,52 @@
+package irc
+
+import (
+       "reflect"
+       "testing"
+)
+
+func TestToString(t *testing.T) {
+       data := []struct {
+               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"},
+       }
+
+       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)
+               }
+       }
+}
+
+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"}},
+       }
+
+       for _, d := range data {
+               str := d.String()
+               out, err := ParseBytes([]byte(str))
+               if err != nil {
+                       t.Errorf("ParseBytes(%#v) = _, %v, but did not expect an error.", str, err)
+                       continue
+               }
+               if !reflect.DeepEqual(out, d) {
+                       t.Errorf("ParseBytes(%#v) = \n%#v\n, but expected \n%#v", str, out, d)
+               }
+       }
+}
diff --git a/irc/parse.go b/irc/parse.go
new file mode 100644 (file)
index 0000000..cf64312
--- /dev/null
@@ -0,0 +1,141 @@
+package irc
+
+import "strings"
+
+// Parse a bytes of octets into a message.
+// The bytes do not end on \r\n, these must be stripped beforehand.
+// This makes it compatible with bufio.ScanLines.
+//
+// This function does not do any error handling (yet) of malformed input
+func ParseBytes(data []byte) (Message, error) {
+       var name, user, host, command string
+       var params []string
+       var trailing string
+       b := strings.Builder{}
+
+       // states
+       const (
+               s_init int = iota
+               s_servername
+               s_user
+               s_host
+               s_command
+               s_params
+               s_param
+               s_trailing
+       )
+
+       flush := func() string {
+               defer b.Reset()
+               return b.String()
+       }
+
+       var skipSpace = false
+       state := s_init
+       for _, c := range data {
+               if c == ' ' && skipSpace {
+                       continue
+               } else {
+                       skipSpace = false
+               }
+
+               switch state {
+
+               case s_init:
+                       if c == ':' {
+                               state = s_servername
+                       } else {
+                               state = s_command
+                               b.WriteByte(c)
+                       }
+
+               case s_servername:
+                       if c == '!' {
+                               state = s_user
+                               name = flush()
+                       } else if c == '@' {
+                               state = s_host
+                               name = flush()
+                       } else if c == ' ' {
+                               skipSpace = true
+                               state = s_command
+                               name = flush()
+                       } else {
+                               b.WriteByte(c)
+                       }
+
+               case s_user:
+                       if c == '@' {
+                               state = s_host
+                               user = flush()
+                       } else if c == ' ' {
+                               skipSpace = true
+                               state = s_command
+                               user = flush()
+                       } else {
+                               b.WriteByte(c)
+                       }
+
+               case s_host:
+                       if c == ' ' {
+                               skipSpace = true
+                               state = s_command
+                               host = flush()
+                       } else {
+                               b.WriteByte(c)
+                       }
+
+               case s_command:
+                       if c == ' ' {
+                               skipSpace = true
+                               state = s_params
+                               command = flush()
+                       } else {
+                               b.WriteByte(c)
+                       }
+
+               case s_params:
+                       if c == ':' {
+                               state = s_trailing
+                       } else {
+                               state = s_param
+                               b.WriteByte(c)
+                       }
+
+               case s_param:
+                       if c == ' ' {
+                               skipSpace = true
+                               state = s_params
+                               params = append(params, flush())
+                       } else {
+                               b.WriteByte(c)
+                       }
+
+               case s_trailing:
+                       b.WriteByte(c)
+
+               default:
+                       panic("Invalid state")
+
+               }
+       }
+
+       switch state {
+       case s_command:
+               command = flush()
+       case s_param:
+               params = append(params, flush())
+       case s_trailing:
+               trailing = flush()
+       }
+
+       res := Message{
+               Prefix{name, user, host},
+               Command(command),
+               Params{
+                       params,
+                       trailing,
+               },
+       }
+       return res, nil
+}
diff --git a/irc/parse_test.go b/irc/parse_test.go
new file mode 100644 (file)
index 0000000..e38810d
--- /dev/null
@@ -0,0 +1,59 @@
+package irc
+
+import (
+       "reflect"
+       "testing"
+)
+
+func TestCorrectParsing(t *testing.T) {
+       data := []struct {
+               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{}}},
+       }
+
+       for _, d := range data {
+               out, err := ParseBytes([]byte(d.in))
+               if err != nil {
+                       t.Errorf("ParseBytes(%#v) = _, %v but didn't expect any error\n", d.in, err)
+               }
+               if !reflect.DeepEqual(d.expected, out) {
+                       t.Errorf("ParseBytes(%#v) = \n%#v\nbut expected \n%#v\n", d.in, out, d.expected)
+               }
+       }
+}
+
+func TestStringAfterParse(t *testing.T) {
+       data := []string{
+               "foo",
+               ":server foo",
+               ":server@host foo",
+               ":server@host foo :trailing",
+               ":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",
+       }
+
+       for _, d := range data {
+               parsed, err := ParseBytes([]byte(d))
+               if err != nil {
+                       t.Errorf("ParseBytes(%#v) = _, %v but didn't expect any error\n", d, err)
+                       continue
+               }
+               out := parsed.String()
+               if out != d {
+                       t.Errorf("ParseBytes(%#v).ToString() = %#v, which isn't the same", d, out)
+               }
+       }
+}
diff --git a/irc/string.go b/irc/string.go
new file mode 100644 (file)
index 0000000..aed67a4
--- /dev/null
@@ -0,0 +1,58 @@
+package irc
+
+import (
+       "io"
+       "strings"
+)
+
+func (c Message) String() string {
+       b := strings.Builder{}
+       // writing to string builder never errors
+       c.WriteTo(&b)
+       return b.String()
+}
+
+// Writes the message to a io.Writer, using WriteString if implemented.
+// Returns total number of bytes written and first error encountered
+func (c Message) WriteTo(w io.Writer) (n int, err error) {
+       n = 0
+       m := 0
+       write := func(s string) {
+               if err == nil {
+                       m, err = io.WriteString(w, s)
+                       n += m
+               }
+       }
+
+       // prefix
+       if c.Prefix.Name != "" {
+               write(":")
+               write(c.Prefix.Name)
+               if c.Prefix.User != "" {
+                       write("!")
+                       write(c.Prefix.User)
+               }
+               if c.Prefix.Host != "" {
+                       write("@")
+                       write(c.Prefix.Host)
+               }
+               write(" ")
+       }
+
+       // command
+       write(string(c.Command))
+
+       // parameters
+       for _, param := range c.Params.Params {
+               write(" ")
+               write(param)
+       }
+
+       // trailing
+       if c.Params.Trailing != "" {
+               write(" ")
+               write(":")
+               write(c.Params.Trailing)
+       }
+       return
+}
diff --git a/x/clichat/chat.go b/x/clichat/chat.go
new file mode 100644 (file)
index 0000000..533849c
--- /dev/null
@@ -0,0 +1,46 @@
+package main
+
+import (
+       "bufio"
+       "io"
+       "os"
+
+       "golang.org/x/net/websocket"
+)
+
+var twitchws = "wss://irc-ws.chat.twitch.tv:443"
+
+var origin = "http://localhost"
+
+func main() {
+       conn, err := websocket.Dial(twitchws, "", origin)
+       if err != nil {
+               panic(err)
+       }
+       defer conn.Close()
+       go func() {
+               _, err := io.Copy(os.Stdout, conn)
+               if err != nil {
+                       panic(err)
+               }
+       }()
+
+       input := readInput()
+       for line := range input {
+               conn.Write([]byte(line))
+               conn.Write([]byte{'\r', '\n'})
+       }
+
+}
+
+func readInput() <-chan string {
+       res := make(chan string)
+       go func() {
+               scanner := bufio.NewScanner(os.Stdin)
+               scanner.Split(bufio.ScanLines)
+               for scanner.Scan() {
+                       res <- scanner.Text()
+               }
+       }()
+       return res
+}