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
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!