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.
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 an IRC server and join a channel (mini IRC client).
- Respond to PING requests (with PONG).
- Parse the URL of the image.
- Access the image.
- Convert the image to ASCII.
- Post the ASCII result in a private message to the user.
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