How to Build an IRC Bot in Python

By | April 7, 2018

Amedeo Clemente Modigliani (1884 – 1920) was one of the greatest Italian painters, his paintings are great inspiration for me, so what does all this has to do with Python and IRC? Today we will discuss how to build an IRC bot in Python, I added a little touch to make it more interesting.

Modigliani, Jeanne Hebuterne (1919)

Modigliani, Jeanne Hebuterne (1919)

Build an IRC Bot in Python

Hello, allow me to introduce you to my personal IRC bot Modigliani, it takes a link of an image (any photo online) and convert it into ASCII Art then send it to the user in a private message, nothing more and nothing less!

You will learn how to:

Connect to IRC Server

We will create a socket to connect to the famous irc.freenode.net IRC server, once connected we will do the following:

  • Change the nick name (optional).
  • Sign in (optional).
  • Join the channel.
  • Send a hello message.

Code: Connecting to IRC Server

import socket 
network = 'irc.freenode.net' 
port = 6667 
channel = 'louvre' 
if __name__ == '__main__': 
    mySock = socket.socket (socket.AF_INET, socket.SOCK_STREAM) 
    mySock.connect((network, port)) 
    mySock.send ('NICK Modigliani\r\n') 
    mySock.send ('USER Modilgiani HCASCII HCART :Python IRC\r\n') 
    mySock.send ('JOIN #%s\r\n'% channel) 
    mySock.send ('PRIVMSG #%s :Hello!\r\n' % channel) 
    while True: 
        buffer = mySock.recv(4096)

As we can see, we will connect to irc.freenode.net, port 6667, in the main function we will create a socket and connect to the server, once connected we will:

  • Create the Nick Modigliani, note that we also provided alternative nicknames just in case that someone else is using it.
  • Create/Join the louvre channel.
  • We send a hello message, just because we don’t want to be rude.
  • We loop forever, waiting for input/packets from the IRC channel.

Respond to PING Requests

Now we have an established connection with the server, we can respond to PING requests with PONG response to keep the connection.

Code: Detecting PING Requests

if buffer.find ( 'PING' ) != -1:
    mySock.send ( 'PONG ' + buffer.split() [ 1 ] + '\r\n' )

The PING request looks something like this:

PING :sendak.freenode.net

sendak can be anything else, means that it is not fixed/static.

Our response should look like this:

PONG :sendak.freenode.net

So to get the “:sendak.freenode.net” part of the request, we need to split the buffer using the space as a delimiter, in this case buffer[0] = ‘PING’ and buffer[1] = ‘:sendak.freenode.net’

We can also add another command for the bot to quit and disconnect !q

if buffer.find ( '!q' ) != -1:
    mySock.send ( "PRIVMSG #" + channel + " :Goodbye!\r\n" )
    mySock.send ( 'QUIT\r\n' )
    mySock.close()
    break

As we can see, the code is very simple, the if statement is in the while loop (that receives the input and saves it in the buffer variable as shown above, we search the buffer for !q (you can enhance this of course), so the script will:

  • Send a Goodbye message.
  • Quit IRC server.
  • Close the socket and terminate the connection.
  • Break the while loop which will cause the program to finish.

Scan for URLs

The command to scan URLs is:

!paint <URL>

Where <URL> is the address of the image, the bot will receive this message in this format (raw data):

:ligeti!~ligeti@unaffiliated/ligeti PRIVMSG #louvre :!paint http://...

Now, to extract user name and URL we need two regex filters:

  • For user: ^(.+?)!
  • For URL: \s:!paint\s*(.+?)$

Code: Parse and Open a URL

import re
## -- in the while statement -- ##
if buffer.find( '!paint' ) != -1:
    regex = '^:(.+?)!'
    pattern = re.compile(regex)
    usr = re.findall(pattern, buffer)
    regex = '\s:!paint\s*(.+?)$'
    pattern = re.compile(regex)
    url = re.findall(pattern, buffer)
    print usr, url

I think the code is clear enough, basic regex filters to get the user name and the URL.

Download the Image

Downloading the image can be done using the urllib2 library, if you want to learn more about urllib2 library you can check the official documentation here.

Code

import cStringIO
import urllib2
from PIL import Image
# ... code ... #
imgfile = cStringIO.StringIO(urllib2.urlopen(str(url[0])).read())
im = Image.open(imgfile)

Details:
cStringIO.StringIO() will read a string buffer.
urllib2.urlopen() will open and download a document from the web, we pass the URL of that source and done.
The .read() method will actually read that file and return back its contents.
Image.open() will open an image file (see later the full code).

Convert an Image to ASCII ART

This is the fun part!!!

We will:

  • We scale the image: im.resize().
  • We convert it to monochrome: im.convert().
  • We convert it to ASCII: the for loops.

Code

import sys
import random
from bisect import bisect
# ... code ... #
scale = "$@B%&WM#ZQL*oahkbdpqwmCUYXzcvunxrft/\|()1{}[]?-_+~<>!lI;:,\"^`'. "
scale= scale[::-1]
zonebounds=range(4,256,4)

try:
    imgfile = cStringIO.StringIO(urllib.urlopen(str(url[0])).read())
    im = Image.open(imgfile)
    height = (im.size[1] * 80) / (im.size[0])
    im = im.resize((100, height), Image.ANTIALIAS)
    im = im.convert('L')
    imgBuf=""
    for y in range(0,im.size[1]):
        imgBuf = ""
        for x in range(0,im.size[0]):
            lum = 255-im.getpixel((x,y))
            row = bisect(zonebounds,lum)
            possibles = scale[row]
            imgBuf =imgBuf+possibles[random.randint(0,len(possibles)-1)]
            print str(imgBuf) + '\n'
except:
    e = sys.exc_info()
    print str(e)

By the way, thanks to Stevendkay for all the tips and help to finish this part!

Display Result in IRC

We have some issues to face here:

  • To avoid spamming the channel, we will post the result in a private conversation.
  • It will be time consuming, because each row of pixels will be followed with 1 second of delay, so that the IRC server will not think that the bot is spamming the channel.

Code

import time
# ... code ... #
mySock.send ('PRIVMSG ' + str(usr[0]) + ' :' + imgBuf  + '\r\n')
time.sleep(1)

Testing and Conclusion

!paint https://pbs.twimg.com/profile_images/378800000368902895/d04e53f66423be1e238804bc3a49b63b_400x400.jpeg

Result:

I used HexChat on Windows (IRC client), but you can use whatever you want of course, I had to make the font smaller to get the whole image fit in the screen.

Finally, I believe that this is a good start, some images will not be as clear as others, and please, if you find something cool post a snapshot of the output in the comments, I recommend that you copy/paste the output in a notepad (I use leafpad, notepad++ or sublime), a white background will give better effect IMHO.

Complete code

import socket
import re
import cStringIO
import urllib2
import sys
import random
import time
from bisect import bisect
from PIL import Image

network = 'irc.freenode.net'
channel = 'louvre'
port = 6667
scale = "$@B%&WM#ZQL*oahkbdpqwmCUYXzcvunxrft/\|()1{}[]?-_+~<>!lI;:,\"^`'. "
scale= scale[::-1]
zonebounds=range(4,256,4)

if __name__ == '__main__':
	mySock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
	mySock.connect((network, port))
	mySock.send ('NICK Modigliani\r\n')
	mySock.send ('USER Modilgiani HCASCII HCART :Python IRC\r\n')
	mySock.send ('JOIN #%s\r\n'% channel)
	mySock.send ('PRIVMSG #%s :Hello!\r\n' % channel)
	while True:
		buffer = mySock.recv(4096)
		if buffer.find ( 'PING' ) != -1:
			mySock.send ( 'PONG ' + buffer.split() [ 1 ] + '\r\n' )
		if buffer.find ( '!q' ) != -1:
			mySock.send ( "PRIVMSG #" + channel + " :Goodbye!\r\n" )
			mySock.send ( 'QUIT\r\n')
			mySock.close()
			break
		if buffer.find( '!paint' ) != -1:
			regex = '^:(.+?)!'
			pattern = re.compile(regex)
			usr = re.findall(pattern, buffer)
			regex = '\s:!paint\s*(.+?)$'
			pattern = re.compile(regex)
			url = re.findall(pattern, buffer)
			print usr, url
			try:
				imgfile = cStringIO.StringIO(urllib2.urlopen(str(url[0])).read())
				im = Image.open(imgfile)
				height = (im.size[1] * 80) / (im.size[0])
				im = im.resize((100, height), Image.ANTIALIAS)
				im = im.convert('L')
				imgBuf=""
				for y in range(0,im.size[1]):
					imgBuf = ""
					for x in range(0,im.size[0]):
						lum=255-im.getpixel((x,y))
						row=bisect(zonebounds,lum)
						possibles=scale[row]
						imgBuf=imgBuf+possibles[random.randint(0,len(possibles)-1)]
					mySock.send ('PRIVMSG ' + str(usr[0]) + ' :' + imgBuf  + '\r\n')
					time.sleep(1)
					print str(imgBuf) + '\n'
			except:
				e = sys.exc_info()
				print str(e)
		print buffer

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.