Building a Twitch Chat Integrated Game

STOMP that IRC

Being in Corona isolation, I have been watching some Twitch streams and was wondering if it would be something I’d like to do as well. I love teaching and since we’re all pretty much stuck in our homes, maybe I should give it a shot. It could be kind of like giving a talk at a conference only more limited in interaction… but that’s what I love most about teaching…interaction.

So, maybe Twitch just isn’t for me… unless… What if I could do something to make Twitch a more interactive place? What if I could somehow create something streamers could use to entertain their viewers in a more interactive way?

Time to build something Twitchy!

The Requirements

To keep myself focused, here are the guidelines I set for myself:

  • The bar of entry should be low;
  • The owner of the stream needs to have control;
  • Whatever I make should not draw attention away from the stream, it should be the stream;

These requirements pushed me towards the following preliminary design:

StreamerAudienceTwitchJavascript+HTMLInteracts withJSON?/Screenreader?Opens websiteStreams website to Twitch

To keep the bar of entry low, I’m going to create some kind of web-based game. Making it in Javascript/HTML makes it more accessible, since all users would need is a browser. I’ve had some previous experience with p5.js, so I’m going to use that again for this project.

All I needed to do then was find a way to integrate with Twitch… somehow.

Read the chat!

Scouting around on some Twitch streams, I quickly noticed that the way most Twich users interact with the streamer is through the chat. If that is the common way of doing things, I’m not going to fight that. I’m going to embrace it.

So now I just need to integrate with the Twitch Chat and use that as input for whatever I’m making. I was a bit worried this might be a difficult task. I really didn’t want to use some screen-scraping tooling or whatnot to get the chat messages. Those feelings quickly went away when I learned the good news:

Twitch uses IRC!

For those of you who might not know this…IRC, or Internet Relay Chat, is an Application Layer Protocol built on TCP. It’s a fairly simple text based protocol which has been around for 30 years now and is still used today.

Nice! Since IRC is so common, I only had to find a way to connect my front-end to the IRC chat and we are good to go. And that is kind of where my original plan fell apart.

You see, IRC uses TCP. Javascript, at least the sort of Javascript running in the browser, can’t make regular TCP socket connections. This may sound surprising with the popularity of WebSockets. WebSockets however always start out as a HTTP request and get “upgraded” to TCP. WebSockets are a separate application layer protocol, just like IRC is an application layer protocol. Unfortunately, they do not magically play well together.

So…back to the drawing board!

The Java IRC Client

It seems like I had no other choice than to put a custom Backend between my Frontend and the Twitch Chat. I chose for a Spring Boot application, always good to keep some familiar technologies close when you are building something unfamiliar.

StreamerAudienceTwitchJavascript+HTMLSpring BootInteracts withIRC ClientWebSocketsOpens websiteStreams website to Twitch

To connect to the Twitch IRC, I decided to use the PircBotX library. I’m not going to duplicate their documentation… but I will show you what settings I used to connect to Twitch.

First off, you’ll need a Twitch account and get an Access Token for your application. This website will give you one if you log in.

Next, you need to setup PircBotX to use the correct configuration.

    String authToken = "oauth:YOUR_TOKEN"; // Your oauth password from http://twitchapps.com/tmi
    String channel = "#tomcools"; // The IRC channel name = Twitch Channel name
    Listener listener = <PIRCBOT Listener> // Extend the ListenerAdapter Class
    
    Configuration configuration = new Configuration.Builder()
            .setAutoNickChange(false) //Twitch doesn't support multiple users
            .setOnJoinWhoEnabled(false) //Twitch doesn't support WHO command
            .setCapEnabled(true)
            .addCapHandler(new EnableCapHandler("twitch.tv/tags"))
            // Twitch by default doesn't send JOIN, PART, and NAMES unless you request it, 
            // see https://dev.twitch.tv/docs/irc/guide/#twitch-irc-capabilities
            .addCapHandler(new EnableCapHandler("twitch.tv/membership"))
            .addServer("irc.twitch.tv")
            .setServerPassword(authToken) 
            .addAutoJoinChannel(channel) //Some twitch channel ex. #tomcools
            .addListener(listener).buildConfiguration();
    
    bot = new PircBotX(configuration);
    bot.startBot();

Next we need to add a Listener which will be called whenever a new message arrives on the IRC channel. There is a convenient abstract class you can inherit. You only need to override the methods you actually want to use. In our case, we’re interested receiving messages and some feedback on connect/disconnect events.

    public class TwitchIrcChatListener extends ListenerAdapter {
        private final ChatMessageListener listener;
    
        public TwitchIrcChatListener(final ChatMessageListener listener) {
            this.listener = listener;
        }
    
        @Override
        public void onGenericMessage(GenericMessageEvent event) {
            listener.onChatMessageReceived(new ChatMessage(event.getUser().getNick(),event.getMessage()));
        }
    
        @Override
        public void onConnect(ConnectEvent event) throws Exception {
            super.onConnect(event);
            listener.onConnect();
        }
    
        @Override
        public void onDisconnect(DisconnectEvent event) throws Exception {
            super.onDisconnect(event);
            listener.onDisconnect();
        }
    }

As you can see, I provided a ChatMessageListener in the constructor and all I do is pass the message on to this listener. The definition of that interface is pretty simple.

    public interface ChatMessageListener {
        void onChatMessageReceived(ChatMessage chatMessage);
        void onConnect();
        void onDisconnect();
    }

While this may seem like an unneeded layer of indirection, it allows us to decouple the rest of the application from the PircBotX framework. This way, we’re free to change it out with anyother framework without the need to refactor too much.


STOMP for the Win

Now that we’re getting the chat messages in the Bootiful backend, we still needed a way to get them to my Javascript Frontend. The game we’re building needs to be usable by multiple different users, each with their own session, their own part of the system. I almost instinctively reached for Websockets to complete that task… but something stopped me.

When doing these projects, I always try to limit the amount of new things I’m learning. Trying to mash too much new shinies in a project always slows me down to a pace which I no longer enjoy. I need a sense of progress. Having the IRC integration behind me, I did feel it was time for something new, specially since I had the feeling Websockets weren’t a good fit.

This is when I discovered Simple Text Orientated Messaging Protocol, or STOMP for short.

STOMP provides an interoperable wire format so that STOMP clients can communicate with any STOMP message broker to provide easy and widespread messaging interoperability among many languages, platforms and brokers.

From: https://stomp.github.io/

STOMP resembles IRC in one very convenient way for our application. They use different terminology for this, but they both allow messages to be sent to a named location.

  • In IRC, messages can be sent to CHANNELS users can JOIN.
  • In STOMP, messages can be sent to DESTINATIONS users can SUBSCRIBE on.

While there are some difference between the two, it’s this similarity that caught my attention. Remember, what I wanted to do originally is to connect my frontend game directly to IRC. What can be done instead is to connect the frontend through STOMP to the Spring Boot backend and use that backend as a Bridge to IRC.

FrontendFrontendBackend (Spring)Backend (Spring)TwitchTwitchTwitch UserTwitch UserStart STOMP Session for Twitch Channel #tomcoolsStart IRC Connection on #tomcoolsTypes in chatIRC MessageSTOMP message on /topic/tomcools

Lucky for us, Spring has built in support for running a STOMP broker.

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
            config.enableSimpleBroker("/topic");
        }
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/stomp-websocket")
                    .setAllowedOrigins("*").withSockJS();
        }
    }

This configures a simple message broker and a STOMP endpoint with SockJS support.

SockJS is a library which provides WebSocket-like features, it even uses WebSockets by default. It will however fallback to different protocols and strategies if WebSockets are not available for whatever reason.


Because the backend will function as a bridge between STOMP and IRC, it is only natural we want to start/stop the IRC connection at the same time the connection with STOMP starts/stops. To achieve this, we can listen to Spring Events which will be fired when a session is started/stopped.

    @Autowired (through constructor in project code)
    private final SimpMessagingTemplate template;
    
    @EventListener
    public void handleSessionConnected(SessionConnectEvent event) {
        // Get channel out of event
        String channel = SessionEventHelper.extractChannel(event);
    
        // Start a new Chatbridge (see code for details)
        chatBridge.start(channel, chatMessage -> {
            // Whenever a message arrives, send it to /topic/CHANNEL_NAME through STOMP
            template.convertAndSend("/topic/" + channel, chatMessage);
        });
    }
    
    @EventListener
    public void handleSessionDisconnect(SessionDisconnectEvent event) {
        // Stop Chatbridge
    }

Whenever an IRC message is received on a certain channel (#channel-name), our bridge will place the message on a STOMP destination with the same name (/topic/channel-name). Keeping the name mapping between IRC and STOMP 1 to 1 makes it just a tad easier to manage.


Connecting the frontend

Now that’s all settled… let’s have a quick look at the front-end.

The way the frontend connects to Spring is by using the endpoint we configured earlier. We use the SockJS library and wrap it with a Stomp Client.

    const socket = new SockJS('/stomp-websocket');
    let stompClient = Stomp.over(socket);
    let twitchHandle = // Read from field;
    stompClient.connect({channel: twitchHandle}, function (frame) {
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/'+twitchHandle, function (msg) {
            let message = JSON.parse(msg.body);
            textReceived(message); // Function our game uses to handle received messages
        });
    });

The Roundup

The rest of the frontend doesn’t matter that much to be honest. It’s just a simple frontend game in Javascript written with p5.js. What does matter is that we have some way to get the Twitch Chat messages all the way to Javascript.

The project is available on my website. Check it out if you’re a streamer. While it is just a simple game, it was fun to see people participate on my own stream.

If you build some cool Twitch Chat integrated game or application after reading this post, I’d love to try it!

Full Code is available on Github. <3

Some usefull resources:

comments powered by Disqus