Twitch TV chat

Recently I read Eight Terminal Utilities Every OS X Command Line User Should Know. I was particularly interested in the “say” command, which is text-to-speech program. Since then, I’ve played with the idea of making it read some community generated content. I did look into some API’s of sites I thought would generate amusing content to listen on Friday nights, like 4chan, but I never really got over the planning phase. But that was until now, when I was sitting the night with couple of my friends, of whom one watched some Twitch.tv stream. I said I could put my laptop to read the comments for you, and so I did.

Not long after googling up Twitch developer channel did I find that they do not provide any kind of API for reading the chat. Therefore, I decided to look up the page source code, which revealed that the content is coming from some Firebase installation. The data from Firebase was then inserted on the page with the help of Ember.js. Though for my misery, the transactions seemed to happen trough flash sockets. This whole dance of realtime tools and API’s meant that it would be no good to just HTTP GET the page, which again meant that I’d need to look into ghost-browsers like PhantomJS or the Gecko equivalent SlimerJS.

I’ve worked with these tools in production, but I every time I end touching the script file I feel like I’m patching my project with chewing-gum. I absolutely hate chewing gum, so this time I felt like I wanted to roll something completely new. There’s also the fact that making any automated tool for a third-party website is something I believe should be avoided like plague. If you are not consuming any API’s, you are not going to have a good time expecting to keep up with the service’s update enrollments. From experience, I can say that you are lucky to survive even two weeks with your hacky way to scraper webpages unless you have something like direct access to the service’s source control, which we do not have. Plus, my friends were already getting disbelieving at this point, as I had not written a single line of code yet. So let’s get to it.

Let’s fix AJAX with AJAX

I hate AJAX, so my idea was to use AJAX to battle AJAX. Ultimately it would consume itself, right? Worth the try, so once again I opened my developer tools, but this time I used jQuery to parse the latest chat message with two lines of code. The output string would then be inserted into jQuery AJAX command, which would hit my local server, which would run the say command.

I found that by popping up the chat window you can fetch any channel’s chat by going to URL like http://www.twitch.tv/{{.Username}}/chat

This would open a window where you could use Chrome developer tools to run a simple JavaScript loop like this:

setInterval(function() {
    var wi = $(".chat-messages .tse-content div .message").last()
    var text = $(wi).text()
    $.get("http://localhost:8080/"+text);
}, 3000)

This would hit the local server of mine

package main

import (
    “net/http”
    “os/exec”
    “fmt”
    “strings”
    “html”
    “log”
)

func main() {
    http.HandleFunc(“/”, func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != “/favicon.ico” {
            cmd := exec.Command(“say”, html.EscapeString(strings.TrimLeft(r.URL.Path, “/”)))
            err := cmd.Start()
            if err != nil {
                panic(err)
            }
        }
        fmt.Fprintf(w, “OK”)
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
} 

Together these scripts would start the mayhem, which however, in it’s current state is limited with 3 second interval. I also only choose channels which have enough people online, so that the same message would not be broadcasted twice.

EDIT

I was later pointed out that you every Twitch room works as an IRC room, so I later updated my code as so:

package main

import (
    "fmt"
    "os/exec"
    "github.com/thoj/go-ircevent"
    "github.com/9uuso/unidecode"
)

var busy bool

func talk(message string) {
    if !busy {
        busy = true
        cmd := exec.Command("say", "-v", "Cellos", message)
        cmd.Start()
        cmd.Wait()
        busy = false
    }
}

func main() {
    con := irc.IRC("9uuso", "9uuso")
    con.Password = ""
    err := con.Connect("irc.twitch.tv:6667")
    if err != nil {
        fmt.Println(err)
        return
    }

    con.AddCallback("001", func(e *irc.Event) {
        con.Join("#nl_kripp")
    })

    con.AddCallback("PRIVMSG", func(event *irc.Event) {
        fmt.Println(event.User+":", unidecode.Unidecode(event.Message()))
        go talk(unidecode.Unidecode(event.Message()))
    })

    con.Loop()
} 

This version of the code not only uses the Cellos voice for extra laughs, but is a lot easier to run and monitor. Thanks to the IRC library and its event based working method, I’m also able to make sure no other comment is being spelled out.