Learn Python by Building an IRC Logging Bot!
When I first sat down to learn Python, I was instantly disappointed with all the the available resources, and the fact that almost none of the tutorials I found seemed to work. In fact I got so frustrated with the language that I shelved the language and went to Ruby for several months. It wasn't until I paid a visit to the Windsor Hackforge and met 3 Python fanatics that I decided to take another look.
What I found during my second look at Python was much different, now knowing a couple avid Python programmers I had people who could answer questions and my problems began to become clear. The source of most of my frustration was caused by the incompatibilities between Python 2 and 3. I always want to keep my software up to date so it meant using Python 3, but pretty much every example, tutorial, and book I looked at was written in Python 2.
Its incredibly frustrating when you cant even print text to the console!
The other side of the frustration was the boring tutorials, I mean how many different ways can you generate or manipulate the Fibonacci sequence anyway? Sure they were full of good information, but when you've learned several languages over the years all these tutorials are just a repetition. I needed something different, interesting, and potentially useful to spend my time working on.
So I set my sights on writing a IRC Bot -- How 1995 is that? -- but it was fun. It dawned on me after that, wouldn't it be great if programming tutorials actually walked you through writing something useful or entertaining? I mean, its one thing to learn that "This is how you declare a variable" or "This is how you write a loop" and completely another thing to see how to implement them in the structure of the program.
I've gone back and re-written my original bot, took a lot of it out and tried to smooth out some of the rough edges, and updated it so it runs properly in Python 3. Without further Ado....
How to write a IRC Logging Bot in Python 3
First things first... lets skip those. I'll assume you have Python 3 already installed on your system and know how to use a text editor to create your script. This is a small program and fits nicely in a single file, so open up your editor of choice and follow along.
At the top of our file we are going to put in our shebang line and import a few packages.
#!/usr/bin/env python3 import sys import socket import string import argparse
Now in order to handle an instance of an ircbot, I'm going to create an object. If your not familiar with OOP (Object Oriented Programming) this may be a bit tricky for you, but Python makes this extremely simple.
We define a new class with the class keyword and the class name as such:
Make a note of that colon. Everything will need to be indented based on it after this. The next thing we are going to do is create a constructor, which will populate some variables and establish the connection to the IRC server.
class ircbot(): def __init__(self, host, port, channel, name, log): self.HOST = host self.PORT = port self.CHANNEL = channel self.NICK = name self.IDENT = name self.REALNAME = name self.LOG = log
The def __init__ you are seeing is a function which is automatically called when the object is created (we will do that later), make note of the "self" in the definition. Its needed by all class functions and global variables, in order for other functions to have access to them.
This isn't the full constructor either, the next thing we are going to put in is the connection to the server. And just so that we don't get any messy errors thrown at us, I'm also going to introduce exception handling via try-except. This isn't in depth, but serves as a good example for your future programs.
def __init__(self, host, port, channel, name, log): # Everything above, after self is a variable we are going # to expect every time a instance of ircbot is created self.HOST = host self.PORT = port self.CHANNEL = channel self.NICK = name self.IDENT = name self.REALNAME = name self.LOG = log try: # Any exception thrown here will halt the program, jumping # down to the except block below self.irc = socket.socket() self.irc.connect((self.HOST, self.PORT)) # If you have played with Python 2 you will notice a difference # in the below print statement, print is now a function print("Connected to %s\n" % self.HOST) self.irc.send(("NICK %s\r\n" % self.NICK).encode()) self.irc.send(("USER %s %s bla :%s\r\n" % (self.IDENT, self.HOST, self.REALNAME)).encode()) self.irc.send(("JOIN %s\r\n" % self.CHANNEL).encode()) print("Joined %s as %s\n" % (self.CHANNEL, self.NICK)) except Exception as e: # Exception is generic all built-in, non-system-exiting # exceptions are derived from this class. # which makes it a simple way to catch whatever and print a # clean message and exit with a system # error code print(e) sys.exit(1)
There, if we created a instance of ircbot, with the correct info in host, port, channel, name and log it would connect to the irc server. It won't do us much good through, its not reading responses or handling the PING/PONG that the IRC server will send to see if we are still connected and it certainly isn't going to write all the activity to a log file for us either. Lets finish this off the ircbot class by creating a "run" function, it doesn't have to be run. In fact it doesn't even have to be a separate function, we could just as easily write this into the __init__, but I don't like to put to much into the constructor if I don't have to.
def run(self): """ Main Application Loop """ # creating a tring variable to dump irc data into readbuffer = "" # Everything the program does is going to happen here # an infinit loop of catching data and parsing it while True: readbuffer = readbuffer + self.irc.recv(1024).decode() # We don't need everything the server sends to us # so we split it into a list and then drop the first part temp = readbuffer.split("\n") readbuffer = temp.pop() # Now we are going to step through the rest of the # lines in the list for line in temp: # Here I am going to strip off line endings # and split the string into a list using # white space as the separator. It's not # necessary, but useful for parsing commands # if you grow the bot that way linex = line.rstrip() linex = linex.split() # PING PONG! # When the IRC Server sends us a ping we # best respond, or it will drop us if (linex == "PING"): self.irc.send(bytes("PONG %s\r\n" % linex)) else: # And here we handle printing to the screen # or to a file if self.LOG == 'stdout': print(line) else: with open(self.LOG, "a") as log: log.write(line)
Now the IRC Logger Bot is complete. You could import this and use it in another program, but lets make it runnable as one file. I'm going to add another function that will use the argparse module to give us some Command line power, as well as give some default settings.
def parseargs(): parser = argparse.ArgumentParser() parser.add_argument('-s', '--server', default='irc.freenode.net', help='DNS address of the IRC server. default=irc.freenode.net') parser.add_argument('-p', '--port', type=int, default=6667, help='port number of irc server. default=6667') parser.add_argument('-c', '--channel', default='#python-unregistered', help='IRC channel to join. default=#python-unregistered') parser.add_argument('-n', '--name', default='irc_logger_bot', help='how the bot will be identified in the channel. default=irc_logger_bot') parser.add_argument('-o', '--output', default='stdout', help='file to write log to. default=stdout')
Most of these parser lines are the same, but take an extra look at the -p/--port option. It has one more configuration flag than the others. I'm sure you can figure that out on your own, Python makes coding easy.
Once we add our final lines to this project, you will be able to run this script with a -h or --help and get a shiny helpful output like:
usage: tutorial_bot.py [-h] [-s SERVER] [-p PORT] [-c CHANNEL] [-n NAME] [-o OUTPUT] optional arguments: -h, --help show this help message and exit -s SERVER, --server SERVER dns address of the irc server. default=irc.freenode.net -p PORT, --port PORT port number of irc server. default=6667 -c CHANNEL, --channel CHANNEL irc channel to join. default=#python-unregistered -n NAME, --name NAME how the bot will be identified in the channel. default=irc_logger_bot -o OUTPUT, --output OUTPUT file to write log to. default=stdout
Finally, we are going to make this run if executed from the command line by using if __name__ == '__main__':
if __name__ == '__main__': # Get the options from the parser opt = parseargs() # Create bot object and run it! bot = ircbot(opt.server, opt.port, opt.channel, opt.name, opt.output) bot.run()
What does if __name__ == '__main__' do?
Well when the python interpreter runs it creates some special variables such as __name__, if a file is executed it is assigned the name __main__. using __name__ == '__main__' in this way allows us to cleanly either import the file as a module, or execute specific code.
And that it, program complete. I hope you have found this post both helpful and useful.
The full code can be found at https://github.com/wsimmerson/ircbot/blob/master/tutorial_bot.py
Thanks for reading!