--- /dev/null
+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
+}
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:]
"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))
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 {
}
}()
- return res
+ return res, errChan
}
--- /dev/null
+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
+}
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) {
"os"
"strings"
+ "fl-gui.name/twitchchat/twitch"
"fl-gui.name/twitchchat/twitch/core"
"fl-gui.name/twitchchat/twitch/core/messages"
)
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() {
}
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 == "" {