From: Fl_GUI Date: Mon, 24 Jun 2024 18:15:59 +0000 (+0200) Subject: reconnecting twitch client X-Git-Url: https://git.openfl.eu/?a=commitdiff_plain;h=2575edefdb9253ec2dcd736e608580e4d8e5fcfc;p=twitch-chat.git reconnecting twitch client --- diff --git a/twitch/chatclient.go b/twitch/chatclient.go new file mode 100644 index 0000000..2cca6d1 --- /dev/null +++ b/twitch/chatclient.go @@ -0,0 +1,121 @@ +package twitch + +import ( + "fmt" + "io" + + "fl-gui.name/twitchchat/twitch/core" + "fl-gui.name/twitchchat/twitch/core/messages" +) + +type ChatClient struct { + Connection *core.Conn + Messages <-chan messages.Message + + // if empty then anonymous login and disregard accessToken + nickname string + accessToken string + + capabilities map[string]struct{} + + logger io.Writer +} + +func NewChatClient() ChatClient { + return ChatClient{nil, nil, "", "", make(map[string]struct{}), io.Discard} +} + +func (c ChatClient) Close() { + c.Connection.Close() +} + +func (c *ChatClient) WithAuthentication(nickname, accessToken string) { + c.nickname = nickname + accessToken = accessToken +} + +func (c *ChatClient) WithoutAuthentication() { + c.nickname = "" +} + +func (c *ChatClient) WithCapability(capabilities ...string) { + for _, capa := range capabilities { + c.capabilities[capa] = struct{}{} + } +} + +func (c *ChatClient) WithLogging(out io.Writer, trace bool) { + c.logger = out + if trace { + core.DebugLogger = c.logger + } +} + +func (c *ChatClient) Connect() error { + conn, err := core.Dial("http://localhost") + if err != nil { + return err + } + c.Connection = conn + defer func() { + if err != nil { + conn.Close() + } + }() + + c.Messages = conn.ReadMessages() + if err = c.authenticate(); err != nil { + return err + } + c.pingPong() + if err = c.askCapabilities(); err != nil { + return err + } + + fmt.Fprintf(c.logger, "connected\n") + return nil +} + +func (c *ChatClient) authenticate() error { + if c.nickname == "" { + msgs, err := c.Connection.AuthenticateAnonymous(c.Messages) + if err != nil { + return err + } else { + c.Messages = msgs + } + } else { + msgs, err := c.Connection.Authenticate(c.nickname, c.accessToken, c.Messages) + if err != nil { + return err + } else { + c.Messages = msgs + } + } + return nil +} + +func (c *ChatClient) pingPong() { + msgs, errs := c.Connection.PingPong(c.Messages) + go func() { + err := <-errs + fmt.Fprintf(c.logger, "ping pong game dropped: %s\n", err) + fmt.Fprintf(c.logger, "reconnecting\n") + c.Connect() + }() + + c.Messages = msgs +} + +func (c *ChatClient) askCapabilities() error { + var caps = make([]string, 0, len(c.capabilities)) + for c, _ := range c.capabilities { + caps = append(caps, c) + } + msgs, err := c.Connection.WithCapability(c.Messages, caps...) + if err != nil { + return err + } + c.Messages = msgs + return nil +} diff --git a/twitch/core/messages/messages.go b/twitch/core/messages/messages.go index 7853225..017694c 100644 --- a/twitch/core/messages/messages.go +++ b/twitch/core/messages/messages.go @@ -22,7 +22,7 @@ func (m Message) Channel() string { if len(m.Params.Params) == 0 { return "" } - if m.Params.Params[0] != "#" { + if m.Params.Params[0][0] != '#' { return "" } return m.Params.Params[0][1:] diff --git a/twitch/core/pingpong.go b/twitch/core/pingpong.go index 68e4265..dc8577b 100644 --- a/twitch/core/pingpong.go +++ b/twitch/core/pingpong.go @@ -8,16 +8,20 @@ import ( "fl-gui.name/twitchchat/twitch/core/messages" ) +var IncorrectPongResponse = errors.New("incorrect twitch pong response") +var NoPongResponse = errors.New("lost pong response") + // Intercept received ping messages and respond to them with an appropriate pong -func (c *Conn) PingPong(msgs <-chan messages.Message) <-chan messages.Message { +func (c *Conn) PingPong(msgs <-chan messages.Message) (<-chan messages.Message, <-chan error) { res := make(chan messages.Message) + errChan := make(chan error) var pingMessage string // send ping go func() { for _ = range time.Tick(time.Minute * 2) { if pingMessage != "" { - panic(errors.New("Ping message ignored by twitch.")) + errChan <- NoPongResponse } pingMessage = randomString(10, AlphaNum) c.WriteMessage(messages.Ping(pingMessage)) @@ -33,6 +37,7 @@ func (c *Conn) PingPong(msgs <-chan messages.Message) <-chan messages.Message { pongText := msg.Text() if pongText != pingMessage { fmt.Fprintf(DebugLogger, "Error: send PING %s but received a PONG %s\n", pingMessage, pongText) + errChan <- IncorrectPongResponse } pingMessage = "" } else { @@ -41,5 +46,5 @@ func (c *Conn) PingPong(msgs <-chan messages.Message) <-chan messages.Message { } }() - return res + return res, errChan } diff --git a/twitch/join.go b/twitch/join.go new file mode 100644 index 0000000..ac46716 --- /dev/null +++ b/twitch/join.go @@ -0,0 +1,30 @@ +package twitch + +import ( + "strings" + + "fl-gui.name/twitchchat/twitch/core" + "fl-gui.name/twitchchat/twitch/core/messages" +) + +func (c ChatClient) Join(channels ...string) (<-chan messages.Message, core.ChannelMembers, error) { + allMesgs, members, err := c.Connection.Join(c.Messages, channels...) + if err != nil { + return nil, members, err + } + channelMessages := make(chan messages.Message) + channelFilter := make(map[string]struct{}) + for _, c := range channels { + lower_c := strings.ToLower(c) + channelFilter[lower_c] = struct{}{} + } + go func() { + for msg := range allMesgs { + _, ok := channelFilter[msg.Channel()] + if ok { + channelMessages <- msg + } + } + }() + return channelMessages, members, nil +} diff --git a/x/corechatclient/main.go b/x/corechatclient/main.go index 7ba156a..ca27314 100644 --- a/x/corechatclient/main.go +++ b/x/corechatclient/main.go @@ -35,7 +35,7 @@ func main() { panic(err) } - msgs = conn.PingPong(msgs) + msgs, _ = conn.PingPong(msgs) if msgs, err = conn.WithCapability(msgs, core.TagsCapability, core.MembershipCapability); err != nil { if errors.Is(err, core.UnsupportedCapabilitiesError) { diff --git a/x/rechat/main.go b/x/rechat/main.go index 7e4b068..bc4b396 100644 --- a/x/rechat/main.go +++ b/x/rechat/main.go @@ -8,6 +8,7 @@ import ( "os" "strings" + "fl-gui.name/twitchchat/twitch" "fl-gui.name/twitchchat/twitch/core" "fl-gui.name/twitchchat/twitch/core/messages" ) @@ -24,29 +25,20 @@ func openFile() (io.WriteCloser, error) { return os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, fs.ModePerm) } -func openChat() (conn *core.Conn, msgs <-chan messages.Message, err error) { - conn, err = core.Dial("http://localhost") - if err != nil { - return - } - +func openChat() (conn twitch.ChatClient, msgs <-chan messages.Message, err error) { + cc := twitch.NewChatClient() if *debug { - core.DebugLogger = os.Stdout + cc.WithLogging(os.Stdout, true) } - msgs = conn.ReadMessages() - msgs, err = conn.AuthenticateAnonymous(msgs) - if err != nil { - return - } - msgs = conn.PingPong(msgs) - msgs, err = conn.WithCapability(msgs, core.MembershipCapability) + cc.WithCapability(core.MembershipCapability) + + err = cc.Connect() if err != nil { - return + return cc, nil, err } - - msgs, _, err = conn.Join(msgs, *channel) - return conn, msgs, err + msgs, _, err = cc.Join(*channel) + return cc, msgs, err } func main() { @@ -58,11 +50,11 @@ func main() { } defer out.Close() - conn, messages, err := openChat() + cc, messages, err := openChat() if err != nil { panic(err) } - defer conn.Close() + defer cc.Close() enc := json.NewEncoder(out) if *outFile == "" {