package chat import ( "encoding/json" "errors" "log" "net/http" "net/url" "strings" "time" "safetwitch-backend/extractor/chat/structs" "github.com/gorilla/websocket" ) var u = url.URL{Scheme: "wss", Host: "irc-ws.chat.twitch.tv:443", Path: "/"} var conn *websocket.Conn var _ *http.Response var err error // If the connection is ready to subscribe to channels var connectionReady = false var streamersFollowing = map[string]bool{} func BeginTwitchChatConnection() { conn, _, err = websocket.DefaultDialer.Dial(u.String(), nil) log.Println("Connecting to Twitch IRC") if err != nil { log.Fatal(err) } defer conn.Close() // Authenticate with server err = sendMessage("CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands") err = sendMessage("PASS none") err = sendMessage("NICK justinfan333333333333") if err != nil { log.Printf("Failed to send message: %v", err) } // Follow streamers for streamer := range streamersFollowing { sendMessage("JOIN #" + streamer) } // Continue to listen for incoming messages from the server. for { _, msg, err := conn.ReadMessage() if err != nil { log.Printf("Failed to read message: %v", err) connectionReady = false expReconnect() break } parseMessage(string(msg)) RemoveUnusedStreamers() } } func expReconnect() { // Attempt to reconnect with exponential backoff waitTime := time.Second maxWaitTime := 30 * time.Second for { log.Printf("Attempting to reconnect in %v...\n", waitTime) time.Sleep(waitTime) BeginTwitchChatConnection() if waitTime < maxWaitTime { waitTime *= 2 } else { waitTime = maxWaitTime } } } func sendMessage(msg string) error { return conn.WriteMessage(websocket.TextMessage, []byte(msg)) } func parseMessage(msg string) error { if !connectionReady && strings.Contains(msg, "001") { connectionReady = true log.Println("Authenticated with Twitch IRC!") return nil } /* example message @badge-info=subscriber/2;badges=subscriber/0,premium/1;client-nonce=a245bea7f04bac06be7bd637144c3721;color=;display-name=EmbeddedMess;emotes=;first-msg=0;flags=16-19:A.3;id=3ad413d1-1ab1-4cd1-902e-020c29a7b45c;mod=0;returning-chatter=0;room-id=227217502;subscriber=1;tmi-sent-ts=1685535959939;turbo=0;user-id=693838516;user-type= :embeddedmess!embeddedmess@embeddedmess.tmi.twitch.tv PRIVMSG #dino_xx :Why doesn't the noob have loot keys on then? */ if !connectionReady { return nil } // Three parts, metadata, middle, message SEE README.md splitMsg := strings.Split(msg, " :") if len(splitMsg) < 3 { return nil } p1 := splitMsg[0] p2 := splitMsg[1] p3 := splitMsg[2] parsedTags := parseTags(p1) p2Split := strings.SplitAfter(p2, " ") username := parseUsername(p2Split[0]) messageType := strings.Replace(p2Split[1], " ", "", 1) channel := strings.Replace(p2Split[2], "#", "", 1) parsedMessage := structs.TwitchMessageMetadata{ Username: username, MessageType: messageType, Channel: channel, Message: p3, Tags: parsedTags, } parsed, err := json.Marshal(parsedMessage) if err != nil { log.Fatalln(err) return err } clientsToSendTo := ClientHandler.FindClientsByStreamer(strings.Replace(channel, " ", "", 1)) for _, Client := range clientsToSendTo { Client.send <- string(parsed) } return nil } func parseUsername(username string) string { return strings.Split(username, "!")[0] } func parseTags(tags string) map[string]string { // Split the data by semicolon separator keyValues := strings.Split(tags, ";") // Create an empty map to store the key-value pairs var m = map[string]string{} // Parse each key-value pair and add it to the map for _, keyValue := range keyValues { pair := strings.SplitN(keyValue, "=", 2) if len(pair) == 2 { m[pair[0]] = pair[1] } } return m } func FollowStreamer(streamerName string) error { err := sendMessage("JOIN #" + strings.ToLower(streamerName)) if err != nil { return err } streamersFollowing[streamerName] = true return nil } func SendMessage(msg string) error { if connectionReady { return conn.WriteMessage(websocket.TextMessage, []byte(msg)) } return errors.New("connection not ready") } // function to check and remove unused streamers from server map func RemoveUnusedStreamers() { ClientHandler.Lock() for streamer := range streamersFollowing { found := false // iterate over each client and their streamer map for client, _ := range ClientHandler.Clients { if client.FollowingStreamers[streamer] { found = true break } } if !found { // remove streamer from server map delete(streamersFollowing, streamer) sendMessage("PART #" + streamer) } } ClientHandler.Unlock() }