I had been wanting to write an IRC bot with asynchronous IO for a while. My bot responds to pings, but can be extended by defining functions and registering them to get called when the bot receives certain commands. My bot uses the RFC 1459 USER command and parameters, but can be made to comply with RFC 2812 by replacing my on_connect function with a compliant one. Here is what I wrote based on the asyncore and asynchat modules:
import asynchat
import asyncore
import collections
import socket
def parse(data):
data = data.split(b' ')
if data[0].startswith(b':'): #prefix present
prefix = data[0][1:]
data = data[1:]
else:
prefix = None
command = data[0]
data = b' '.join(data[1:])
if data.startswith(b':'): #only trailing parameter
parameters = [data[1:]]
else:
data = data.split(b' :')
if len(data) > 1: #trailing parameter present
trailing = b' :'.join(data[1:])
else:
trailing = None
parameters = data[0].split(b' ') #regular parameters
if trailing is not None:
parameters.append(trailing) #add trailing parameter to regular parameters
return prefix, command, parameters
class Connection(asynchat.async_chat):
def __init__(self, nick, user, realname, host, port=6667):
self.nick = nick
self.user = user
self.realname = realname
self.address = (host, port)
self.received = list()
asynchat.async_chat.__init__(self)
self.set_terminator(b'\n')
self.handlers = collections.defaultdict(set)
def collect_incoming_data(self, data):
self.received.append(data)
def found_terminator(self):
data = b''.join(self.received).rstrip(b'\r')
del self.received[:]
prefix, command, parameters = parse(data)
try:
for handler in self.handlers[command]:
handler(self, prefix, parameters)
except Exception as exception:
for handler in self.handlers[exception]:
handler(self, exception)
def message(self, string):
string = ''.join((string, '\r\n'))
self.push(string.encode())
def establish(self):
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect(self.address)
#We use a "plugin" oriented system so we can be flexible with how we handle events, messages, and errors.
def handle(self, command, handler):
self.handlers[command.encode()].add(handler)
def ignore(self, command, handler):
self.handlers[command.encode()].discard(handler)
def subscribe(self, topic, handler):
self.handlers[topic].add(handler)
def unsubscribe(self, topic, handler):
self.handlers[topic].discard(handler)
def handle_connect(self):
try:
for handler in self.handlers['connect']:
handler(self)
except Exception as exception:
for handler in self.handlers[exception]:
handler(self, exception)
if __name__ == '__main__':
def on_keyerror(connection, exception):
pass
def pong(connection, prefix, parameters):
connection.message('PONG {0}'.format(parameters[0]))
def on_connect(connection):
connection.message('NICK {0}'.format(connection.nick))
connection.message('USER {0} {1} bla :{2}'.format(connection.user, connection.address[0], connection.realname))
c = Connection('lrh9bot', 'lrh9bot', 'lrh9bot', 'irc.dal.net')
c.subscribe(KeyError, on_keyerror)
c.handle('PING', pong)
c.subscribe('connect', on_connect)
c.establish()
asyncore.loop()