--- /dev/null
+import tornado.iostream
+import socket
+
+_MAX_MESSAGE_OCTETS = 512
+
+class IRCClient(object):
+ """
+ A terribly-incomplete async IRC client, built atop tornado's IOStream.
+
+ Right now it mostly just connects to a server, identifies, parses
+ commands, and answers pings.
+ """
+ def __init__(self, host="127.0.0.1", port=6667, tracing=False):
+ self.username = b"amy" # pay homage to those who came before us
+ self.host = host
+ self.port = port
+ self.socket = None
+ self.stream = None
+ self.tracing = tracing
+
+ def __repr__(self):
+ return "<IRCClient to {}:{}>".format(self.host, self.port)
+
+ def connect(self):
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+ self.stream = tornado.iostream.IOStream(self.socket)
+ self.stream.connect((self.host, self.port), self._on_connect)
+
+ def _on_connect(self):
+ # Send initial login commands
+ self._send_nick()
+ self._send_user()
+ # Set up reply handler
+ self.stream.read_until(b'\r\n', self._on_line_received)
+
+ def _send_nick(self):
+ # NICK <nickname>
+ self.send_command(b'NICK', self.username)
+
+ def _send_user(self):
+ # USER <username> <hostname> <servername> <realname>
+ self.send_command(b'USER', self.username, b'localhost', b'localhost', b'Web gateway')
+
+ def _on_line_received(self, data):
+ """
+ This function is the primary state machine dispatcher.
+ """
+ if self.tracing:
+ print b"read:",
+ print b">>", data,
+
+ # Parse and dispatch the line (minus the CRLF).
+ self._parse_and_handle(data[:-2])
+
+ # Schedule self to handle next line too.
+ self.stream.read_until(b'\r\n', self._on_line_received)
+
+ def _parse_and_handle(self, data):
+ # Parse line and run the action handler.
+ fragment = data
+
+ # Extract prefix, if present
+ if fragment.startswith(b':'):
+ prefix, fragment = fragment.split(b' ', 1)
+ fragment.lstrip()
+ else:
+ prefix = ""
+
+ # Note that command can be either textual or 3-digit numeric
+ command, fragment = fragment.split(b' ', 1)
+ fragment.lstrip()
+
+ # Extract list of params
+ params = []
+ while fragment:
+ if fragment.startswith(b':'):
+ # Trailing param, consumes the rest of the line
+ params.append(fragment[1:])
+ break
+ # "middle" param
+ if b' ' in fragment:
+ param, fragment = fragment.split(b' ', 1)
+ fragment.lstrip()
+ params.append(param)
+ else:
+ params.append(fragment)
+ break
+
+ self.handle_command(prefix, command, params)
+
+ def handle_command(self, prefix, command, params):
+ """
+ The IRC-command level message handler. This can be overridden by a
+ child class which wants to hook in additional handlers.
+ """
+ if self.tracing:
+ print b"Parsed line:"
+ print b"\tprefix:", prefix
+ print b"\tcommand:", command
+ print b"\tparams:"
+ for p in params:
+ print b'\t\t', p
+ # Handle pings.
+ if command == b"PING":
+ self.send_command(b"PONG", *params)
+
+ # TODO: implement more actions in response to interesting commands
+ # TODO: consider table-driven event handling?
+
+ def send_command(self, command, *params):
+ # RFC 1459 limits client commands to strictly textual commands
+ if not command.isalpha():
+ raise ValueError(b"Asked to send command '{}', but client commands must match [A-Za-z]+".format(command))
+ # TODO: check that command is a known command from the RFC?
+
+ # RFC 1459 limits params to 15 items
+ if len(params) > 15:
+ raise ValueError(b"Parameters list must be 15 items or less (yours was {})".format(len(params)))
+
+ # Check that params don't include any forbidden characters
+ for i in xrange(len(params)):
+ if i != len(params)-1:
+ if " " in params[i]:
+ raise ValueError(b"Whitespace not allowed in parameters other than the final one (found whitespace in parameter {}: '{}')".format(i, params[i]))
+ if "\x00" in params[i]:
+ raise ValueError(b"NUL not allowed in parameter values (found in parameter {}: '{}')".format(i, params[i]))
+ if "\r" in params[i]:
+ raise ValueError(b"CR not allowed in parameter values (found in parameter {}: '{}')".format(i, params[i]))
+ if "\n" in params[i]:
+ raise ValueError(b"LF not allowed in parameter values (found in parameter {}: '{}')".format(i, params[i]))
+
+ # N.B. *args come in as a tuple, not a list, and tuples are immutable
+ paramlist = list(params)
+ if len(params) > 0:
+ paramlist[-1] = b':' + paramlist[-1]
+ paramstring = b" ".join(paramlist)
+ msg = b"{} {}\r\n".format(command, paramstring)
+
+ # Once serialized, verify that the command itself fits in the RFC's message length limit
+ if len(msg) > _MAX_MESSAGE_OCTETS:
+ raise ValueError(u"message was too long: {} octets (max allowed by IRC spec is {})".format(len(msg), _MAX_MESSAGE_OCTETS))
+
+ # Actually write the message to the stream.
+ if self.tracing:
+ print "write: <<", msg,
+ self.stream.write(msg)
+
+if __name__ == "__main__":
+ ircclient = IRCClient(tracing=True)
+ ircclient.connect()
+ import tornado.ioloop
+ tornado.ioloop.IOLoop.instance().start()