.hack//tk

.hack//tk

yet another technology blog

Creating a simple IRC Bot in Python

IRC, Internet Relay Chat, is a text chat program created in 1988. Although this technology is outdated, many IRC servers still continue to exist with thousands of users discussing different topics and sharing information. Mostly, IRC is used among developers. There are many open source projects IRC channels on the internet; even on private servers, developers from huge companies still prefer to communicate using IRC channels.

An IRC Bot is basically a program that is going to parse and interpret what the users from IRC channels are sending directly or indirectly to this bot. This could be an open message on the channel or a message directly to the bot, that is going to be parsed and interpreted to execute commands, log the chat in a file or even reply to some questions the users might send.

For this IRC Bot we are going to use Python 3 and mainly its socket library to handle the low-level networking communication.

Connecting to an IRC channel

First, you need to choose a server and a channel to connect. It is recommended to use a channel name that does not exist. By creating a new channel you will be the only user on that channel, which will allow you to not affect other users with the tests. If you do not have a preference for IRC servers, try Freenode IRC Server.

At the top, we are going to define some constant variables that will store the connection information. After, we will define a function initializeBot() that is going to connect the bot to the server and channel you defined previously.

Here, we are going to define the IRC object. It is going to be a socket to connect and communicate with the IRC server. Sockets serve for many purposes. In this case, we are going to use it to establish a continuous connection with the IRC server while the bot is sending and receiving information.

After connecting we need to send the IRC server some information about the bot with the send() command, such as the nickname, ident, realname. Make sure that before sending the information you add a sleep() command of 5 seconds, In some servers the bot might take a while to connect, so you need to make sure it is already connected to the server to send the command to join a channel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SERVER = 'chat.freenode.net'
PORT = 6697
CHANNEL = '$(YOUR_CHANNEL)'

MASTER = '$(YOUR_NAME)'
NICK = '$(YOUR_BOT_NICKNAME)'
IDENT = '$(YOUR_BOT_NICKNAME)'
REALNAME = '$(YOUR_BOT_NICKNAME)'

IRC = None

def initializeBot():
global IRC

IRC = wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM))
IRC.connect((SERVER, PORT))

# In some servers the bot takes a while to connect, we need to wait to join a channel
sleep(5)

IRC.send(bytes('NICK %s\r\n' % NICK, 'UTF-8'))
IRC.send(bytes('USER %s %s bla :%s\r\n' % (IDENT, SERVER, REALNAME), 'UTF-8'))
IRC.send(bytes('JOIN %s\r\n' % CHANNEL, 'UTF-8'))

More info: Python Sockets

Event Loop

The event loop, also known as the main loop, will be used to check if some message has been sent to the channel that our IRC bot is connected to and handle the message properly later on. In order to achieve that we first need to create a loop that is going to be checking for messages. We do not want this loop to be checking non-stop, which is why we are putting a sleep() command for this loop to run each half a second. This way we will not require as much processing power and as much delay to reply.

Each half a second, we will get what our socket received using the recv() command. It is recommended to use a relatively small power of 2 number, such as 4096 to be the buffer size. Decode it from bytes object to human readable text and save it on a variable called readBuffer, splitting each line we received into a string list to be passed to the function that is going to handle each line of text individually.

1
2
3
4
5
6
7
8
9
10
11
12
13
if __name__ == '__main__':
initializeBot()

readBuffer = ''
while True:
sleep(0.5)

readBuffer = readBuffer + IRC.recv(4096).decode('UTF-8')
stringList = str.split(readBuffer, '\n')
readBuffer = stringList.pop()

for line in stringList:
handleLine(line)

More info: Event Loop, Encoding/Decoding

Handling the message

For each message we receive, we are going to handle it using the function below. First we use rstrip() and split() string handling functions to trim default trailing characters from the end of our message and then break down this message into smaller chunks (strings), which we will use inside of an array received by the split() function.

1
2
3
def handleLine(line):
line = str.rstrip(line)
line = str.split(line)

More info: Common String Operations

Ping Pong

IRC servers send out PING message from time to time to check if the users are still connected. One must respond to that message with PONG, and that’s what we are doing in the code below, we send a PONG right away. This is defined by the IRC Client Protocol.

1
2
if line[0] == 'PING':
IRC.send(bytes('PONG %s\r\n' % line[1], 'UTF-8'))

More info: IRC Client Protocol

Identifying the message

For the other messages, first, we must identify who’s the sender and get the complete message before going to the next step. As the protocol implies, the format for messages from users to channels and to another users is source PRIVMSG :Message. We are using two auxiliar functions in order to identify these parts, getSenderName() and getMessage() based on the format.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def getSenderName(line):
sender = ''
for c in line:
if(c == '!'):
break
if(c != ':'):
sender += c
return sender

def getMessage(line):
message = ''
for messagePiece in line:
message += messagePiece + ' '
return message.lstrip(':')

Now we are making usage of these functions to identify the sender and the message, and logging messages sent and received to the standard output.

1
2
3
4
5
6
7
8
if line[1] == 'PRIVMSG':
if (line[2] == NICK):
return

sender = getSenderName(line[0])
message = getMessage(line[3:])

print('Message from ' + sender + ' received: ' + message)

More info: Common String Operations, Standard Streams

Parsing the message

In the next step we are going to interpret the message from the user to our bot and reply. In this case, we are replying if someone asks the bot who's your master or a similar phrase containing the keyword master in it. We are going to reply with the name we set in the MASTER variable. Otherwise we will reply with a default message.

Note that I am using lower() string handling function since we want to compare the strings in case insensitive.

1
2
3
4
def parseMessage(message):
if 'master' in message.lower():
return MASTER
return 'Ahora estoy ocupado, alguien tiene que trabajar aquí!'

Now we are making usage of this function to send the user a message only if the message sent to the channel begins with $(YOUR_BOT_NICKNAME). You could reply to any message, but normally IRC Bots are requested to answer something only when called. Otherwise it would be annoying for the users on that channel.

1
2
if message.startswith(NICK):
sendMessage(parseMessage(message), CHANNEL)

More info: Parsing, Case Sensitivity

Ready!

This concludes our simple IRC bot in Python, if you are interested in this topic you can search for popular IRC Bots on Github. There are plenty of open source IRC Bot projects in different languages. You can find the complete code for this bot on my Github Repository