Coffee Space


Listen:

IRC Bot

Preview Image

Preview Image

TL;DR

Some of the advice online about building IRC bots is slightly wrong, so here is a slightly less wrong way of doing it. The basic principles will be in Java - but could easily be anything else.

Introduction

Internet Relay Chat (IRC) dates back to 1988 according to Wikipedia, as a program to allow BBS users to instant message one another (replacing a previous janky method).

An IRC bot is just an IRC client, where the client is actually just a bot (robot), therefore you can use the associated documentation. Everything you technically need is here (archive).

That document is quite long and you can get away with a hell of a lot less. There's only really only a few features you need to support for plain text IRC:

Admin

As is to be expected with Java, there is admin that needs to be done in order to be able to use it. We need to include the following:

import java.io.IOException;           // Encase we get an I/O error
import java.net.Socket;               // The socket we'll chat on
import java.net.UnknownHostException; // Encase we can't find the server

We have the following variables we want to use globally (with some example values):

String server = "irc.example.com"; // The server we're connecting to
int port = 6667;                   // The port for plain text (good default)
String channel = "#channel";       // The channel we're connecting to
Socket socket = null;              // The socket we'll communcate on
String user = "bot-face";          // What we call ourselves

Socket Connect

Some more Java admin - we need to open the socket (yes there really is that many potential exceptions):

try{
  socket = new Socket(server, port);
}catch(UnknownHostException e){
  System.err.println("Could not determine the IP of host '" + server + "'");
}catch(IOException e){
  System.err.println("Unable to create socket");
}catch(SecurityException e){
  System.err.println("Security issue raised whilst creating socket");
}catch(IllegalArgumentException e){
  System.err.println("Invalid port number");
}

Now we're ready to talk to the server.

Basic Send/Get

IRC (without security) is simply plain text. The protocol is human readable, you can literally use the server with just nc (netcat). As the RFC says:

The IRC protocol is a text-based protocol, with the simplest client being any socket program capable of connecting to the server.

We start by defining a function to send lines to the server (note the new line characters):

/**
 * sendLine()
 *
 * Send a line on the socket.
 *
 * @param data The data to be sent.
 **/
private void sendLine(String data){
  if(socket.isConnected()){
    try{
      socket.getOutputStream().write(data.getBytes());
      socket.getOutputStream().write('\n');
      socket.getOutputStream().flush();
    }catch(IOException e){
      System.err.println("Failed to write to socket");
    }
  }
}

And a function to read lines from the server:

/**
 * getLine()
 *
 * Get a line from the socket.
 *
 * @return The received line, otherwise NULL.
 **/
private String getLine(){
  if(socket.isConnected()){
    byte[] buff = new byte[256];
    int i = 0;
    int c;
    try{
      while((c = socket.getInputStream().read()) >= 0){
        /* Check for newline character */
        if(c == '\n' || c == '\0'){
          buff[i++] = '\0';
          break;
        }
        /* Store the bytes */
        if(i < buff.length - 1){
          buff[i++] = (byte)c;
        }
      }
    }catch(IOException e){
      System.err.println("Failed to read from socket");
    }
    return new String(buff);
  }else{
    return null;
  }
}

We read characters one by one as the server could potentially split a message over more than one packet. There are possible more efficiernt ways to do this, but we don't want to allow ourselves to read too many characters in some kind of attack.

Authentication

From the RFC protocol definition:

      Command: USER
   Parameters: <username> <hostname> <servername> <realname>

We can rubbish our way through this by simply using the user value for each. The server does no real checking, this is only for the purpose of providing information.

Next we send the nickname we want to be referred as, again we just use user and ignore hopcount:

      Command: NICK
   Parameters: <nickname> [ <hopcount> ]

This can be implemented like so:

if(socket != null && socket.isConnected()){ // Check that we could connect
  sendLine("USER " + user + " " + user + " " + user + ": " + user);
  sendLine("NICK " + user);
  server = getLine().split(" ")[0].replace(":", "");
}else{
  socket = null; // Indicate that we were unable to connect
}

See that we set server to a new value? The IRC server will probably connect us to one of their internal servers in order to distribute the load. We should now communicate with that (no need to create a new socket). It's likely we'll be sent somewhere different each time.

Note that we don't send a channel connection request in this step. Some servers will not let you join a channel until they have authenticated you internally. We therefore send it later.

Main Loop

There are just a few more concepts we must be aware of. First is a "ping", it's where the server is making sure our client is still active and not just connected for no reason. We'll recieve these periodically, but they may appear in any line - so we have to silter them out. If you don't respond for a while, the server will kick you out.

      Command: PING
    Parameters: <server1> [<server2>]

And the correct response to a "ping", is a "pong":

      Command: PONG
   Parameters: <daemon> [<daemon2>]

At the same time, we also request to join a channel:

      Command: JOIN
   Parameters: <channel>{,<channel>} [<key>{,<key>}]

Technically you should only have to join once... But. Because we don't listen to the "MOTD" (message of the day) end, we don't know if we were accepted and therefore connect the join request at the correct time. It's also possible that our bot gets kicked out of the channel. For simplicty, we just send it every time we have to pong back at the server. (All tested servers had no problem with this, so don't @ me.)

In the RFC the protocol is as follows:

      Command: PRIVMSG
    Parameters: <receiver>{,<receiver>} <text to be sent>

We are only interested in one case for now, messages in the channel. Therefore we want the format ("direct" messages are a different format):

PRIVMSG <channel> <server> :<msg>

And finally the code for the main loop:

if(socket != null){ // Make sure there is a server
  String in = null; // Pre-define a variable we will always use
  boolean running = true; // Possibility of stopping the client
  while(running){ // Bot should reply to all messages
    try{ // We don't want our client to crash if we wrote bad code
      in = getLine(); // Get a line from the server
      if(in.contains("PING")){ // Did the server ping us?
        String[] resp = in.split(" "); // Get parts of ping
        if(resp.length >= 2){ // Make sure we heard a valid ping
          sendLine("PONG " + resp[1] + "\r"); // Send the ping response
          sendLine("JOIN " + channel); // Re-join channel encase of disconnect
        }else{
          System.err.println("Unable to PONG the IRC server");
        }
      }else{ // We likely just heard a message
        int s = in.indexOf(':');
        int e = in.indexOf(':', s + 1);
        if(s >= 0 && e >= 0){ // Did we understand the message?
          String[] head = in.substring(s + 1, e).split(" "); // Get info
          if(
            head.length == 3          && // Make sure there were enough parts
            head[1].equals("PRIVMSG") && // Make sure it was a message
            !head[0].equals(server) // Check it was from the server
          ){
            String usr = head[0].split("!")[0]; // Who sent it?
            if(!usr.equals(user)){ // Don't reply to ourself
              String msg = in.substring(e + 1); // Get the actual message
              /* TODO: Process the message and response here! */
              sendLine("PRIVMSG " + channel + " :" + msg); // Echo message back
            }
          }
        }
      }
    }catch(Exception e){
      System.err.println("Bot tried to die! Line: " + in); // Print offender
      e.printStackTrace(); // Information about crash location
    }
  }
}else{
  System.err.println("Unable to run server"); // Say something went wrong
}

That's all, folks!