Minecraft to IRC bridge

Like almost 2.5 million others, my recent productivity has taken a hit due to the game Minecraft. I have a home server set up to play with roommates and friends. Funnily enough, Minecraft turned into a frequent medium of communication for us. To take part in this when not logged into minecraft, and to get notifications to when friends were playing, I set out to make a bridge between minecraft and IRC.

The result is mcirc, a simple minecraft to irc gateway. It tails the minecraft log looking for player joins quits and messages, and relays them to the IRC server. This script requires that there is a FIFO for input to the server.

It is worth noting that this probably not the best minecraft <-> irc bridge which exists, but perhaps the simplest, and runs on a vanilla minecraft server. There are several which function as bukkit plugins: 1 2 3 .

Usage is

ruby irc.rb -s irc.freenode.org -c balmoralmc -f minecraft/console.pipe -l minecraft/server.log

The script itself is very small and largely command line parsing

#!/usr/bin/env ruby

require 'socket'
require 'uri'

class MinecraftIrcBot
  def initialize(options)
    uri = URI.parse("irc://#{options[:server]}")
    @channel = options[:channel]
    @socket = TCPSocket.open(uri.host, uri.port || 6667)
    @mclog = IO.popen("tail -f -n0 '#{options[:log]}'", "r")
    @name = options[:name]
    @pipe = options[:pipe]
    say "NICK #{@name}"
    say "USER #{@name} 0 * #{@name}"
    say "JOIN ##{@channel}"
  end

  def say(msg)
    puts msg
    @socket.puts msg
  end

  def say_to_chan(msg)
    say "PRIVMSG ##{@channel} :#{msg}"
  end

  def say_to_minecraft(msg)
    msg = "say #{msg}"
    puts msg

    File.open(@pipe, "w") do |console|
      console.puts msg
    end
  end

  def run
    loop do
      read, write, error = IO.select([@socket, @mclog])
      if read.include? @socket
        msg = @socket.gets

        # connection lost
        return unless msg

        case msg.strip
        when /^PING :(.+)$/i
          say "PONG #{$1}"
        when /^:(.+?)!.+?@.+?\sPRIVMSG\s.+?\s:(.+)$/i
          say_to_minecraft("<#{$1}> #{$2}")
        end
      end

      if read.include? @mclog
        msg = @mclog.gets
        msg.gsub!(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} /, '')
        case msg.strip
        when /^\[INFO\] ([a-z0-9]*) lost connection/i
          say_to_chan("#{$1} has left")
        when /^\[INFO\] ([a-z0-9]*) \[[^\]]*\] logged in/i
          say_to_chan("#{$1} has joined")
        when /^\[INFO\] <([a-z0-9]*)> (.*)$/i
          say_to_chan("<#{$1}> #{$2}")
        end
      end
    end
  end

  def quit
    say "PART ##{@channel} :bye bye"
    say 'QUIT'
  end
end


def parse
  require 'optparse'
  options = {}
  optparse = OptionParser.new do |opts|
    opts.banner = "Usage: #{$0} options"

    opts.on("-s", "--server SERVER", "IRC server") do |v|
      options[:server] = v
    end
    opts.on("-c", "--channel CHAN", "IRC channel") do |v|
      options[:channel] = v
    end
    opts.on("-f", "--fifo FIFO", "named pipe into minecraft server's console") do |v|
      options[:pipe] = v
    end
    opts.on("-l", "--log LOG", "minecraft servers's log file for reading") do |v|
      options[:log] = v
    end

    options[:name] = "mcirc"
    opts.on("-n", "--name NAME", "name of the irc user") do |v|
      options[:name] = v
    end
  end
  begin
    optparse.parse!

    required = [:server, :channel, :pipe, :log]
    required.each do |arg|
      raise OptionParser::MissingArgument, arg if options[arg].nil?
    end
  rescue OptionParser::InvalidOption, OptionParser::MissingArgument
    puts $!.to_s
    puts optparse
    exit 1
  end
  options
end

def run
  options = parse
  bot = MinecraftIrcBot.new(options)

  trap("INT"){ bot.quit }

  bot.run
end

run