Coffee Space


Listen:

IRC Bot

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:

0001 import java.io.IOException;           // Encase we get an I/O error
0002 import java.net.Socket;               // The socket we'll chat on
0003 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):

0004 String server = "irc.example.com"; // The server we're connecting to
0005 int port = 6667;                   // The port for plain text (good default)
0006 String channel = "#channel";       // The channel we're connecting to
0007 Socket socket = null;              // The socket we'll communcate on
0008 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):

0009 try{
0010   socket = new Socket(server, port);
0011 }catch(UnknownHostException e){
0012   System.err.println("Could not determine the IP of host '" + server + "'");
0013 }catch(IOException e){
0014   System.err.println("Unable to create socket");
0015 }catch(SecurityException e){
0016   System.err.println("Security issue raised whilst creating socket");
0017 }catch(IllegalArgumentException e){
0018   System.err.println("Invalid port number");
0019 }

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):

0020 /**
0021  * sendLine()
0022  *
0023  * Send a line on the socket.
0024  *
0025  * @param data The data to be sent.
0026  **/
0027 private void sendLine(String data){
0028   if(socket.isConnected()){
0029     try{
0030       socket.getOutputStream().write(data.getBytes());
0031       socket.getOutputStream().write('\n');
0032       socket.getOutputStream().flush();
0033     }catch(IOException e){
0034       System.err.println("Failed to write to socket");
0035     }
0036   }
0037 }

And a function to read lines from the server:

0038 /**
0039  * getLine()
0040  *
0041  * Get a line from the socket.
0042  *
0043  * @return The received line, otherwise NULL.
0044  **/
0045 private String getLine(){
0046   if(socket.isConnected()){
0047     byte[] buff = new byte[256];
0048     int i = 0;
0049     int c;
0050     try{
0051       while((c = socket.getInputStream().read()) >= 0){
0052         /* Check for newline character */
0053         if(c == '\n' || c == '\0'){
0054           buff[i++] = '\0';
0055           break;
0056         }
0057         /* Store the bytes */
0058         if(i < buff.length - 1){
0059           buff[i++] = (byte)c;
0060         }
0061       }
0062     }catch(IOException e){
0063       System.err.println("Failed to read from socket");
0064     }
0065     return new String(buff);
0066   }else{
0067     return null;
0068   }
0069 }

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:

0070       Command: USER
0071    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:

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

This can be implemented like so:

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

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.

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

And the correct response to a “ping”, is a “pong”:

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

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

0085       Command: JOIN
0086    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:

0087       Command: PRIVMSG
0088     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):

0089 PRIVMSG <channel> <server> :<msg>

And finally the code for the main loop:

0090 if(socket != null){ // Make sure there is a server
0091   String in = null; // Pre-define a variable we will always use
0092   boolean running = true; // Possibility of stopping the client
0093   while(running){ // Bot should reply to all messages
0094     try{ // We don't want our client to crash if we wrote bad code
0095       in = getLine(); // Get a line from the server
0096       if(in.contains("PING")){ // Did the server ping us?
0097         String[] resp = in.split(" "); // Get parts of ping
0098         if(resp.length >= 2){ // Make sure we heard a valid ping
0099           sendLine("PONG " + resp[1] + "\r"); // Send the ping response
0100           sendLine("JOIN " + channel); // Re-join channel encase of disconnect
0101         }else{
0102           System.err.println("Unable to PONG the IRC server");
0103         }
0104       }else{ // We likely just heard a message
0105         int s = in.indexOf(':');
0106         int e = in.indexOf(':', s + 1);
0107         if(s >= 0 && e >= 0){ // Did we understand the message?
0108           String[] head = in.substring(s + 1, e).split(" "); // Get info
0109           if(
0110             head.length == 3          && // Make sure there were enough parts
0111             head[1].equals("PRIVMSG") && // Make sure it was a message
0112             !head[0].equals(server) // Check it was from the server
0113           ){
0114             String usr = head[0].split("!")[0]; // Who sent it?
0115             if(!usr.equals(user)){ // Don't reply to ourself
0116               String msg = in.substring(e + 1); // Get the actual message
0117               /* TODO: Process the message and response here! */
0118               sendLine("PRIVMSG " + channel + " :" + msg); // Echo message back
0119             }
0120           }
0121         }
0122       }
0123     }catch(Exception e){
0124       System.err.println("Bot tried to die! Line: " + in); // Print offender
0125       e.printStackTrace(); // Information about crash location
0126     }
0127   }
0128 }else{
0129   System.err.println("Unable to run server"); // Say something went wrong
0130 }

That’s all, folks!