A Console IRC Bot.

I'm planning to write some articles based on questions I get from fellow students.

The first one is how to get on IRC with C#?. So, here it is, the first article of (hopefully) many.

Let's start by telling what IRC is. This is best done by reading the Internet Relay Chat Protocol RFC. You can find anything you want about the IRC protocol in there.

Getting on IRC is as simple as:
  • Establishing a connection.
  • Logging in.
  • Maintaining a connection and reacting to commands.
As this is an article on how to establish an IRC connection and work with the commands, I'm not going to spent any time on UI. Therefore this will be a simple Console Application (cIRC).

We'll make a seperate class for the IRC functions so we could re-use it later when we want to add a UI.

using System;

using System.Net;

using System.Net.Sockets;

using System.IO;

 

namespace System.Net {

      public class IRC {

 

      } /* IRC */

} /* System.Net */


Needed info.

Let's think about what info we need for a connection.

First of all, the server and port ofcourse. Then the nickname to be used, and also the userinfo. The userinfo contains the actual username (used in ident), and the Real Name (which you can see when doing a WHOIS).

So, add the IrcServer, IrcPort, IrcNick, IrcUser and IrcRealName properties.

For this example I'm going to create a single channel IRC bot. So we can also add an IrcChannel property. If you would take this further you would split that off. The first thing I'm thinking of is, create a channel object and manage each channel you're on with one of those.

I also added a bool IsInvisible property, as this is the only mode you can set as a normal user (See 3.1.5 User mode message).

The Connection.

Now that we have all our info to make our first connection, let's implement it.

I'll be using this application as an example on how to create events as well.

To start our bot, we will just have one event. The eventReceiving. This will occur every time a command gets received from the IRC server. For now we'll just write it to the console.

What we need is the following. After our namespace we add:

public delegate void CommandReceived(string IrcCommand);

And right after our class we add:

 

public event CommandReceived eventReceiving;


This is how our application will look as a start:

 

using System;

using System.Net;

 

namespace cIRC {

      class cIRC {

            static void Main(string[] args) {

                  IRC cIRC = new IRC("CumpsD", "#mypreciousss");

                  cIRC.eventReceiving += new CommandReceived(IrcCommandReceived);

                  cIRC.Connect("efnet.xs4all.nl", 6667);

            } /* Main */

           

            static void IrcCommandReceived(string IrcCommand) {

                  Console.WriteLine(IrcCommand);

            } /* IrcCommandReceived */

      } /* cIRC */

} /* cIRC */


As you can see, we have bound the eventReceiving to a local method, which will handle the data.

I'll supply the source at the end of the article so you can check out the constructor and other details yourself.

The logic behind our bot is that after we launch the .Connect on it, it keeps running, and fires off events when it detects a command. For this article I'll display everything to the console in a nice format.

First, we connect with the server and register ourself.

// Connect with the IRC server.

this.IrcConnection = new TcpClient(this.IrcServer, this.IrcPort);

this.IrcStream = this.IrcConnection.GetStream();

this.IrcReader = new StreamReader(this.IrcStream);

this.IrcWriter = new StreamWriter(this.IrcStream);

 

// Authenticate our user

string isInvisible = this.IsInvisble ? "8" : "0";

this.IrcWriter.WriteLine(String.Format("USER {0} {1} * :{2}", this.IrcUser, isInvisible, this.IrcRealName));

this.IrcWriter.Flush();

this.IrcWriter.WriteLine(String.Format("NICK {0}", this.IrcNick));

this.IrcWriter.Flush();

this.IrcWriter.WriteLine(String.Format("JOIN {0}", this.IrcChannel));

this.IrcWriter.Flush();

I don't have any error handling when you pick an already chosen nick. You can implement that in the listener loop and abort the connection, let the user choose another nick, and retry.

After we are connected there is a listening loop which looks like:

// Listen for commands

while (true) {

      string ircCommand;           

      while ((ircCommand = this.IrcReader.ReadLine()) != null) {

            if (eventReceiving != null) { this.eventReceiving(ircCommand); }

                             

            string[] commandParts = new string[ircCommand.Split(' ').Length];

            commandParts = ircCommand.Split(' ');

            if (commandParts[0].Substring(0, 1) == ":") {

                  commandParts[0] = commandParts[0].Remove(0, 1);

            }

                 

            if (commandParts[0] == this.IrcServer) {

                  // Server message

                  switch (commandParts[1]) {

                        case "332": this.IrcTopic(commandParts); break;

                        case "333": this.IrcTopicOwner(commandParts); break;

                        case "353": this.IrcNamesList(commandParts); break;

                        case "366": /*this.IrcEndNamesList(commandParts);*/ break;

                        case "372": /*this.IrcMOTD(commandParts);*/ break;

                        case "376": /*this.IrcEndMOTD(commandParts);*/ break;

                        default: this.IrcServerMessage(commandParts); break;

                  }

            } else if (commandParts[0] == "PING") {

                  // Server PING, send PONG back

                  this.IrcPing(commandParts);

            } else {

                  // Normal message

                  string commandAction = commandParts[1];

                  switch (commandAction) {

                        case "JOIN": this.IrcJoin(commandParts); break;

                        case "PART": this.IrcPart(commandParts); break;

                        case "MODE": this.IrcMode(commandParts); break;

                        case "NICK": this.IrcNick(commandParts); break;

                        case "KICK": this.IrcKick(commandParts); break;

                        case "QUIT": this.IrcQuit(commandParts); break;

                  }

            }

      }

 

      this.IrcWriter.Close();

      this.IrcReader.Close();

      this.IrcConnection.Close();

}

What is happing here is:

First we fetch a command coming from the server.

Then we split it up into parts, delimited by a space and then we decide what action to take depending on wether it's a server command or a normal user mode command.

The server and user codes can be found in the RFC if you want to add more.

Responding to commands.

My plan was just to display data, but let me show you how you can respond to commands. I know this could be solved cleaner, but I'm writing this as a proof of concept and to explain what would be required and how it works.

We want to welcome everyone joining a channel. But we want it by notice. When someone joins the IrcJoin method gets called:

private void IrcJoin(string[] IrcCommand) {

      string IrcChannel = IrcCommand[2];

      string IrcUser = IrcCommand[0].Split('!')[0];

      if (eventJoin != null) { this.eventJoin(IrcChannel.Remove(0, 1), IrcUser); }

} /* IrcJoin */


Which in turns fires the join event and gets processed in our console app:

private void IrcJoin(string IrcChan, string IrcUser) {

      Console.WriteLine(String.Format("{0} joins {1}", IrcUser, IrcChan));

      IrcObject.IrcWriter.WriteLine(String.Format("NOTICE {0} :Hello {0}, welcome to {1}!", IrcUser, IrcChan));

      IrcObject.IrcWriter.Flush ();

} /* IrcJoin */


I modified our console app a bit so it would create an object of itself in the Main with our IrcObject as a private variable. That way I can access the IrcWriter I created in there. Ofcourse it is a bad idea to make that writer public. A better practice would be to create methods like NoticeUser, KickUser, etc... to control it's behaviour. But that exceeds the purpose of this article.

This concludes how you use C# to get on IRC.

Here are some ideas where you could extend this application:

  • Listen for certain triggers in the channel, then do some action. (Example: '!google searchterm', have your app do a query and reply the results)
  • Make this bot an opp and listen in PRIVMSG for a user to authenticate and op him. (Authenticate him against Active Directory, that way you'll learn how to work with AD as well)
  • Detect kicks against trusted users and take action to prevent takeovers, or auto rejoin when the bot gets kicked.
  • ...
Hopefully this answered the question, feel free to contact me when you have any questions.

I uploaded the source so you could learn from them. Enjoy!

12 Comments

  • well this is some very nice source code you posted here.

    i think a lot of people can learn from it

    thanks!

  • Muchos graçias senior,



    I'll check this source out, but It won't be for today, maybe in a couple of hours, (tomorrow :-p)...



  • Thanks for the source code, I'm using it for some stuff I'm playing with. So far it's working perfectly.



  • Interesting code, however I was connecting to quakenet (ie se.quakenet.org, port 6667) and couldnt get the JOIN to work until I received "376" (End of /MOTD command). I rewrote the class for my purposes (ie to generate a stubborn back-talking BOT and also converting it to VB.NET) and fixed that...so it works!



    But there is one more issue, the swedish characters. They get lost somehow in the way.



    I was thinking that this line:

    m_tcpReader = New System.IO.StreamReader(m_tcpStream)



    should read:

    m_tcpReader = New System.IO.StreamReader(m_tcpStream, System.Text.Encoding.UTF8)



    (and same changed for the m_tcpWriter line) but it caused even more problems than it solved (somehow it fails already on the PONG-return).



    Is there another System.Text.Encoding I should choose, or is the m_tcpWriter.WriteLine causing the trouble (ie should it be cr+lf or cr only or lf only?)...or something completely other thing?



    I guess I could upload some VB-sample code if it would help...or I guess the same things would apply for your source as well?



    Best regards,

    Svarvsven

  • Ok, interesting. I'll try the Encoding.Default! Yes, about the JOIN part...I fixed that. :)



    Tell me if you want to have a look of the VB-version...



    Best regards,

    Svarvsven (skom_svarvsven@yahoo.com)

  • Yes, the System.Text.Encoding.Default worked really nice. Thanks! (and thanks for such a quick answer too)

  • I am pretty mucho interested in the vb.net / vb version of the code.



    Any way to get a copy of it ?

  • Hi,



    I did the changes for the JOIN problem, however I still can't connect to othernet, whereas I can connect to quakenet.

    Any ideas?



    Regards,

    tom (tom.frey@greentreetrading.com)

  • If there is interest in my vb-port of the bot, please let me know. I did send S Reisinger a copy.



    skom_svarvsven@yahoo.com

  • just to let you know, I figured out the problem now.

    It's in here:

    if (commandParts[0].IndexOf(this.IrcServer) != -1)

    because if you connect to a server like irc.othernet.org or irc.quakenet.org you'll actually get redirected to another server like "Warren.MS.US.Othernet.org" and that's why the whole switch segment never gets triggered

  • an ircbot glined me how can i get through it? please help me if you can this is my e-mail if any one can help me it will be greatly appriciated - shane-



    skullraider144@yahoo.com

  • hello,
    im getting the error that i cannot write to a closed writer on this line of code

    while ((ircCommand = this.reader.ReadLine()) != null)

    this is under
    //Listen For Commands.

    im not writing in Console so its kinda different and i dont Communicate with IRC yetesablish a connection without any displaying of messages.

    if someonecould help

    AOL- Vincecarter15x15@aol.com
    AIM- skatecrashrepeat
    Alternative Email- Punk123@myway.com


    thanks
    -stephen

Comments have been disabled for this content.