Create an IRC Bot with Python 3
Jun 08, 2023 • 0 Minute Read
We’ve blogged some about IRC before and talked about the #LinuxAcademy channel on the Freenode IRC network. Our students have made it very clear on multiple times how much they want more Python from us as well. As we've previously announced, more is coming from our new instructor, Shiraz. Until he is ready to release his new Python content, however, I thought I might help tide you over with a fun Python project that I enjoy: Making an IRC bot.In this blog post, I’m going to cover how to create a basic IRC bot with Python 3. We’re going to assume a basic knowledge of Python and functions for this post, and if you’ve taken our Introduction to Python on Linux course you should have all the knowledge you need for this basic IRC bot tutorial. Feel free to comment below or hop on the #LinuxAcademy channel on Freenode if you have any questions or need any help. I go by OrderChaos on IRC (and most other places) so look for that name!
IRC Bots
Before we get started, you might be wondering, "What is an IRC bot?" The ever-useful and wonderful Wikipedia defines an IRC Bot as “a set of scripts or an independent program that connects to Internet Relay Chat as a client, and so appears to other IRC users as another user. An IRC bot differs from a regular client in that instead of providing interactive access to IRC for a human user, it performs automated functions.” (Source)So basically, an IRC bot appears as another user to everyone else, but instead performs set actions per its script in response to predetermined events (usually specific messages in the chat). They can serve a variety of purposes: From saving chat logs, to administrative tools, to any number of other silly, useful, or sometimes strange features. There are some bots that can read Twitter feeds, perform web searches, or even dosed
-like find and replace (I've written one for this and have it shared on GitHub! See the Wrap Up section below for details).IRC Bots can be written in a variety of languages. PHP and Python are two common ones, but there are ones in many other languages as well, including C, Java, and even Haskell. Wikipedia has a page listing a comparison of many IRC bots. Many of them are now defunct and no longer in development or use, but they can serve as inspiration and be interesting to read about.Okay, enough background. Let's get to the cool part: Creating your own IRC bot.
Preparing the Bot
Before we get to start writing all the cool and fun things for our bot to do, first we have to prepare the bot itself. We’ll need to make sure we’ve got Python set up, assigned a few variables, and made sure we can connect to the IRC network. A final note before we get started, attached to this post I’ll include the full code that we go through — if you get lost while coding, just check against the example to see where you should be. The explanations for all the code is included in the comments on the attached bot so you have all the information you need in that file.Python Setup
First, make sure you have Python 3 installed. We’re going to assume you got it installed and configured prior to this post.Open your favorite Python IDE (if you don’t have one, a plain text editor, or even vim in a terminal works). To start with, we should make sure to specify we’re using Python 3. For Windows, this means naming the file something that ends in .py. For Linux/Mac, include this line at the top of the file:#!/usr/bin/python3
.After specifying Python3 we need to import the ‘socket’ library. The socket library is used for connecting and communicating over a network port. We use this to connect to and communicate with the IRC server. Importing libraries is very easy in Python and, as the socket library is built-in, you don’t need to install anything. Just add import socket
beneath the #!/usr/bin/python3
line.So right now your file should only contain these two lines:
#!/usr/bin/python3import socket
Global Variables
Now that we’ve specified we’re working in Python3 and have the socket library imported, we need to define some variables. These variables are used throughout the bot, so we want them to be defined before we write any functions.The first variable is the socket we are using to connect and communicate with the IRC server. Sockets are complicated and can be used for many tasks in many ways. See here if you’d like more information on sockets: https://docs.python.org/3/howto/sockets.html. For now, just know that this is used to establish a continuous connection with the IRC server while the bot is running, to send and receive information.Next is the name of the server and channel to which we are connecting. We could hard code these, but having a variable makes a couple of steps easier. For example, if we ever want to connect to a list of channels (instead of just one, as in this example) or change to a different server or channel we don’t have to find every instance and can just edit this variable instead. I’m using chat.freenode.net for this example. For other IRC networks, just put in the name in the same location.After that, we define the channel we're going to eventually join. We don’t want to use an official/established channel while we do our testing. Aside from being rude, many channels have specific rules for bots or don’t allow them at all. Make sure you check with a channel’s moderators before adding your bot to a channel. For our testing, we’re using a custom, unregistered room (on Freenode, denoted by the ‘##’ before the channel name). This way we’ll be the only ones in the channel with the bot while we do our testing.The botnick is what we are naming the bot. It is how other users see the bot in the channel. Make sure this is an unused and unregistered nick, as your bot won’t be able to connect if it’s already in use and it will instead be assigned a random name if it’s a registered and protected nick. See here for more information on nickname registrationThe last two variables will be used in our main function. It’s not required in an IRC bot, but the function we're going to use needs it. All we’re doing is defining a nickname that can send administrative commands to the bot and an exit code to look for to end the bot script. We get to how to do this at the end of the main function.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server = "chat.freenode.net" # Serverchannel = "##bot-testing" # Channelbotnick = "IamaPythonBot" # Your bots nickadminname = "OrderChaos" #Your IRC nickname. On IRC (and most other places) I go by OrderChaos so that’s what I am using for this example.exitcode = "bye " + botnick
Connecting to IRC
Now that we’ve prepared the bot and established a few variables that we are using, let’s get to the actual connection part.To connect to IRC we need to use our socket variable (ircsock) and connect to the server. IRC is typically on port 6667 or 6697 (6697 is usually for IRC with SSL, which is more secure). We are using 6667 in our example. We need to have the server name (established in our global variables) and the port number inside parentheses so it gets passed as a single item to the connection. Once we’ve established the connection, we need to send some information to the server to let the server know who we are. We do this by sending our user information followed by setting the nickname we’d like to go by. Usually these are the same, but registered users sometimes have several nicknames tied to their account and can choose any of them.ircsock.connect((server, 6667)) # Here we connect to the server using the port 6667ircsock.send(bytes("USER "+ botnick +" "+ botnick +" "+ botnick + " " + botnick + "n", "UTF-8")) #We are basically filling out a form with this line and saying to set all the fields to the bot nickname.ircsock.send(bytes("NICK "+ botnick +"n", "UTF-8")) # assign the nick to the bot
Defining Functions
Here we define the various functions our bot uses. These are sections of code that may need to be called on multiple times.Channel Join
Connecting to IRC is good, but unless we’re in a channel with other users it won’t be of much use! We’re putting this in a function instead of hard coding it like the ircsock.connect part above because you can be in multiple channels with a single connection. We’re only going to use one for this example, so it can be hard coded, but this way you can see how it’s done and easily modify it for multiple channels if you’d like.Here we take in the channel name, as part of the function, then send the join command to the IRC network.The 'bytes' part lets us specify to send the message encoded as UTF-8. This is to explicitly send the correct encoding to the IRC server. In Python 2, this isn’t necessary, but changes to string encoding in Python 3 makes this a requirement. You see this whenever we send data to the IRC server. Something else to note, the "n” at the end of the message is a new line character. This is equivalent to pressing the Enter key in a chat window. It lets the server know we’re finished with that command rather than chaining all the commands onto the same line.After sending the join command, we want to start a loop to continually check for and receive new messages from the server until we get a message with ‘End of /NAMES list.’. This indicates we have successfully joined the channel. The details of how each function works is described in the main function section below.def joinchan(chan): # join channel(s). ircsock.send(bytes("JOIN "+ chan +"n", "UTF-8")) ircmsg = "" while ircmsg.find("End of /NAMES list.") == -1: ircmsg = ircsock.recv(2048).decode("UTF-8") ircmsg = ircmsg.strip('nr') print(ircmsg)
Ping Pong
No, I don’t mean the tabletop game. It is common for IRC servers to periodically send out ‘PING’ signals to connected users to make sure they’re still connected. We must respond to these to let the server know we’re still there. If we don’t respond to these signals, we can get disconnected from the server because it assumes we have dropped the connection.This function doesn’t need to take any arguments as the response is always the same. Just respond with PONG to any PING. Different servers have different requirements for responses to PING so you may need to adjust/update this depending on your server. I’ve used this particular example with Freenode and have never had any issues.def ping(): # respond to server Pings. ircsock.send(bytes("PONG :pingisn", "UTF-8"))
Send a Message
We’ve done all the major preparations, now let’s write some functions so our bot actually has something to do. This function will let us send a message to a channel or a user. All we need for this function is to accept a variable with the message we’ll be sending and who we’re sending it to.Using target=channel in the parameters section sets the default value of the 'target' variable to the channel global variable. If only one parameter, msg, is passed to the function it uses the default value for 'target'.The ":” between target and msg lets the server separate the target and the message.def sendmsg(msg, target=channel): # sends messages to the target. ircsock.send(bytes("PRIVMSG "+ target +" :"+ msg +"n", "UTF-8"))
Main Function
Okay, now that we’ve got our active function and have all the connection information prepared, it’s time to write the continuous part of the bot. This is the main function of the bot. It will call the other functions as necessary and process the information received from IRC and determine what to do with it.Starting up
We start by joining the channel we defined in the Global Variables section. After that we start an infinite loop to continually check for and receive new info from server. This ensures our connection stays open. We don’t want to call main() again because, aside from trying to rejoin the channel continuously, you run into problems when recursively calling a function too many times in a row. An infinite while loop works better in this case.def main(): joinchan(channel) while 1:
Receiving information
Here we take in the information sent to us from the IRC server. IRC sends out information encoded in UTF-8 characters so we’re telling our socket connection to receive up to 2048 bytes and decode it as UTF-8 characters. We then assign it to the ircmsg variable for processing.After that, remove any line break characters from the string. If someone types in "n” to the channel, it will still include it in the message just fine. This only strips out the special characters that can cause problems with processing.We also print the received information to the terminal. You can skip this if you don’t want to see it, but it helps with debugging and to make sure the bot is working.ircmsg = ircsock.recv(2048).decode("UTF-8") ircmsg = ircmsg.strip('nr') print(ircmsg)
Split Received Message
Next, check if the information we received includes PRIVMSG in the text. PRIVMSG is how standard messages in the channel (and direct messages to the bot) come in. Most of the processing of messages is in this section.If it is a PRIVMSG, we want to get the nick of the person who sent the message and split it from the message. Messages come in from from IRC in the format of ":[Nick]!~[hostname]@[IP Address] PRIVMSG [channel] :[message]” so we split it for the different parts and assign them to separate variables.if ircmsg.find("PRIVMSG") != -1: name = ircmsg.split('!',1)[0][1:] message = ircmsg.split('PRIVMSG',1)[1].split(':',1)[1]
Choose an Action
Now that we have the name information in it's own variable, we check if the name is less than 17 characters. Usernames (at least for Freenode) are limited to 16 characters. So with this check we make sure we’re not responding to an invalid user or some other message that just happens to have 'PRIVMSG' in it. After that, we use a detection block to see if it includes certain text that the bot should then take action on.With the first stanza, we’re looking to see if someone says Hi to the bot anywhere in their message and then replying. Since we don’t define a target, it will get sent to the channel.The second is an example of how to look for a ‘command’ at the beginning of a message and parse it to do a complex task. In this case, we’re looking for a message starting with ".tell” and using that as a code to look for a message and a specific target to which to send. The whole message should look like ".tell [target] [message]” to work properly. There are comments in the attached bot file that explains how it works in detail.if len(name) < 17: if message.find('Hi ' + botnick) != -1: sendmsg("Hello " + name + "!") if message[:5].find('.tell') != -1: target = message.split(' ', 1)[1] if target.find(' ') != -1: message = target.split(' ', 1)[1] target = target.split(' ')[0] else: target = name message = "Could not parse. The message should be in the format of ‘.tell [target] [message]’ to work properly." sendmsg(message, target)
Stopping the Bot
Since we created an infinite loop in this function, there is no natural end. Instead, we’re going to check for some text and use that to end the function (which automatically ends the loop).What we do is look to see if the name of the person sending the message matches the admin name we defined earlier. We make both lowercase in case the admin typed their name a little differently when joining. On IRC, ‘OrderChaos’ and ‘orderchaos’ are the same nickname, but Python will interpret them as different strings unless we convert it to all lowercase first.We also make sure the message matches the exit code above. The exit code and the message must be EXACTLY the same. This way the admin can still type the exit code with extra text to explain it or talk about it to other users and it won’t cause the bot to quit. The only adjustment we're making is to strip off any whitespace at the end of the message. So if the message matches, but has an extra space at the end, it still works.If the exit code is sent by the admin, the function hits a 'return' line which automatically breaks out of any loops and if statements and goes to the line where the function was called. Normally it continues with additional lines of code, but we're going to end the script on the call to main() so there won't be any more code for it to run through and the bot will close.if name.lower() == adminname.lower() and message.rstrip() == exitcode: sendmsg("oh...okay. :'(") ircsock.send(bytes("QUIT n", "UTF-8")) return else:
Respond to Pings
If the message is not a PRIVMSG it still might need some response. If the information we received was a PING request we call the ping() function we defined earlier to respond with a PONG. This lets the server know we're still online and connected.if ircmsg.find("PING :") != -1: ping()
Start the Main Function
Finally, now that the main function is defined, we need some code to get it started. It doesn't take any parameters, so it's as simple as this line.main()