~/home/blogs/building-a-twitch-chat-bot-with-irc-and-ruby.md

Building a Headless Twitch Chat Bot with Ruby and IRC

A walkthrough of leveraging Twitch's IRC protocol to build a custom, headless auto-greeting bot in Ruby, evolving from a HexChat plugin to a standalone app.

- Raul G.
2021-01-20

The Challenge: Automating Chat Interaction

During a period of Twitch platform instability, I discovered that its chat system is built on the Internet Relay Chat (IRC) protocol. This realization opened a door for automation. By connecting directly to Twitch's IRC servers, I could create a bot to interact with chat channels, independent of the web interface.

My initial goal was simple: automatically greet new users who joined a chat. This post details the journey from a simple client-side plugin to a robust, headless Ruby application running on a Raspberry Pi.

Step 1: Understanding the Connection

Connecting to Twitch IRC is straightforward. The official documentation provides the necessary details:

  • Server: irc.chat.twitch.tv
  • Port: 6697 (for SSL)
  • Authentication: OAuth token (which can be securely obtained from the network traffic of your browser's developer tools while logged into Twitch).

Using a standard IRC client like HexChat, I could join any channel (e.g., #channelname) and see raw IRC events, including user JOIN messages, which are normally hidden in the standard Twitch UI.

Step 2: Proof of Concept with a HexChat Plugin

HexChat supports scripting in Lua, which was perfect for a quick prototype. The API provides a hexchat.hook_print("Join", on_join) function, which allows you to execute a callback whenever a "Join" event occurs.

My first script had two key functions:

  1. on_join(info): This function would trigger when a user joined, extract their nickname, and send a greeting message to the channel.
  2. Remembering Users: To avoid spamming users who frequently reconnect, the script needed to track who it had already greeted. I implemented a simple file-based memory system, creating a file like channel_username.mem for each user greeted.

Here is a snippet of the initial Lua proof of concept:

hexchat.register("auto-greeter", "0.1", "Welcomes new members")

-- A simple file-based check to see if we've met the user before
local function file_exists(name)
   local f=io.open(name,"r")
   if f~=nil then io.close(f) return true else return false end
end

local function on_join(info)
   local nick = info[1]
   local chan = info[2]
   local memory_file = "C:\\Path\\To\\HexChat\\" .. chan .. "_" .. nick .. ".mem"

   if file_exists(memory_file) == false then
      -- Create the memory file and send a greeting
      touch_file(memory_file)
      hexchat.command("MSG " .. chan .. " " .. "Welcome, " .. nick .. "!")
   end
   return hexchat.EAT_NONE
end

hexchat.hook_print("Join", on_join)

This worked, but it had limitations. It was dependent on the HexChat client, and Lua's limited file system support made more complex logic difficult. It was time to build a proper, standalone application.

Step 3: Building a Headless Bot in Ruby

I chose Ruby for its strong networking libraries and my familiarity with the language. The goal was to create a "headless" bot that could run on a server (like a Raspberry Pi) without a GUI.

This approach offered several advantages:

  • Independence: No longer reliant on a specific IRC client.
  • Full Control: Direct manipulation of the IRC protocol.
  • 24/7 Operation: The bot could be containerized and run continuously.

The Ruby script connects to the Twitch IRC server using a standard TCP socket. It handles the initial authentication (PASS and NICK commands) and then enters a loop to read messages from the server.

Evolving the Logic

Moving to a more powerful language allowed for significant improvements. Based on feedback that greeting "lurkers" (users who don't chat) could be intrusive, I changed the core logic.

  • Trigger on First Message, Not Join: Instead of hooking the JOIN event, the bot now listens for PRIVMSG (a user message). It only greets a user after they've sent their first message in a session.
  • Smarter Memory: The file-based memory system was enhanced. The bot now tracks the timestamp of the last interaction, allowing it to use different greeting messages for new users versus returning users.

The core of the Ruby bot is built using the Cinch IRC library, which simplifies connecting to the server and handling events. The main logic is contained within a run_bot.rb script.

require 'cinch'
require_relative 'lib/helpers.rb'

# --- Configuration ---
username = get_arg(0) # Bot's Twitch username
password = get_arg(1) # Bot's OAuth token
input_channels = read_config("./channels.txt")
input_greetings = read_config("./greetings.txt")
input_return_greetings = read_config("./return_greetings.txt")

# --- Bot Setup ---
bot = Cinch::Bot.new do
  configure do |c|
    c.server = "irc.chat.twitch.tv"
    c.port = 6697
    c.ssl.use = true
    c.nick = username
    c.password = "oauth:#{password}"
    c.channels = input_channels
  end

  # Request Twitch-specific capabilities
  on :connect do
    bot.irc.send("CAP REQ :twitch.tv/membership")
  end

  # --- Main Logic: Greeting Users ---
  on :message do |m|
    # Don't greet self or the channel bot
    next if m.user.nick == bot.nick || m.user.nick == m.channel.name[1..-1]

    memory_file = "met/#{m.channel.name}/#{m.user.nick}.mem"
    now = Time.now

    # Check if we've greeted this user recently
    if File.exist?(memory_file)
      last_greeted_time = File.mtime(memory_file)
      # Only greet if it's been more than 12 hours
      if now > last_greeted_time + (12 * 60 * 60)
        m.reply "Welcome back, #{m.user.nick}! #{input_return_greetings.sample}"
        FileUtils.touch(memory_file) # Update timestamp
      end
    else
      # First time greeting, create the memory file
      FileUtils.mkdir_p(File.dirname(memory_file))
      FileUtils.touch(memory_file)
      m.reply "Welcome, #{m.user.nick}! #{input_greetings.sample}"
    end
  end
end

bot.start

Conclusion

This project was a fantastic exercise in practical automation. It started with simple curiosity and evolved from a basic client plugin into a standalone, headless application. By moving to Ruby, I gained greater control and flexibility, allowing for more nuanced and respectful bot behavior. The final bot now runs quietly on a Raspberry Pi, a testament to the power of understanding the protocols that underpin the web services we use every day.

You can find the source code for the Ruby bot on my GitHub.

Share this post