diff --git a/ircbot.py b/ircbot.py index 72b9cf4..8b64a34 100644 --- a/ircbot.py +++ b/ircbot.py @@ -7,6 +7,8 @@ from collections import namedtuple import channel from constants import logmessage_types, internal_submessage_types, controlmessage_types +import line_handling + Server = namedtuple('Server', ['host', 'port']) # ServerThread(server, control_socket) @@ -28,15 +30,17 @@ class ServerThread(threading.Thread): with self.server_socket_write_lock: self.server_socket.sendall(line + b'\r\n') - self.logging_channel.send((logmessage_types.sent, line.decode(encoding = 'utf-8', errors = 'replace'))) + # Don't log PONGs + if not (len(line) >= 5 and line[:5] == b'PONG '): + self.logging_channel.send((logmessage_types.sent, line.decode(encoding = 'utf-8', errors = 'replace'))) def handle_line(self, line): command, _, arguments = line.partition(b' ') if command.upper() == b'PING': self.send_line_raw(b'PONG ' + arguments) else: - # TODO: implement line handling self.logging_channel.send((logmessage_types.received, line.decode(encoding = 'utf-8', errors = 'replace'))) + line_handling.handle_line(line, irc = self.api) def mainloop(self): # Register both the server socket and the control channel to or polling object @@ -80,7 +84,8 @@ class ServerThread(threading.Thread): self.send_line_raw(line) else: - self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error)) + error_message = 'Unknown control message: %s' % repr((command_type, *arguments)) + self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, error_message)) else: assert False #unreachable @@ -92,10 +97,13 @@ class ServerThread(threading.Thread): self.server_socket = socket.create_connection(address) except ConnectionRefusedError: # Tell controller we failed - self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error)) + self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, "Can't connect to %s:%s" % address)) self.logging_channel.send((logmessage_types.internal, internal_submessage_types.quit)) return + # Create an API object to give to outside line handler + self.api = line_handling.API(self) + # Run initialization # TODO: read nick/username/etc. from a config self.send_line_raw(b'NICK HynneFlip') @@ -130,16 +138,20 @@ if __name__ == '__main__': data = logging_channel.recv(blocking = False) if data == None: break - message_type, message_data = data + message_type, *message_data = data if message_type == logmessage_types.sent: - print('>' + message_data) + assert len(message_data) == 1 + print('>' + message_data[0]) elif message_type == logmessage_types.received: - print('<' + message_data) + assert len(message_data) == 1 + print('<' + message_data[0]) elif message_type == logmessage_types.internal: - if message_data == internal_submessage_types.quit: + if message_data[0] == internal_submessage_types.quit: + assert len(message_data) == 1 print('--- Quit') - elif message_data == internal_submessage_types.error: - print('--- Error') + elif message_data[0] == internal_submessage_types.error: + assert len(message_data) == 2 + print('--- Error', message_data[1]) else: print('--- ???', message_data) else: diff --git a/line_handling.py b/line_handling.py new file mode 100644 index 0000000..d650360 --- /dev/null +++ b/line_handling.py @@ -0,0 +1,124 @@ +import constants + +class API: + def __init__(self, serverthread_object): + # We need to access the internal functions of the ServerThread object in order to send lines etc. + self.serverthread_object = serverthread_object + + def send(self, line): + self.serverthread_object.send_line_raw(line) + + def msg(self, recipient, message): + """Make sending PRIVMSGs much nicer""" + line = 'PRIVMSG ' + recipient + ' :' + message + self.serverthread_object.send_line_raw(line) + + def error(self, message): + self.serverthread_object.logging_channel.send((constants.logmessage_types.internal, constants.internal_submessage_types.error, message)) + +class LineParsingError(Exception): None + +# parse_line(line) → prefix, command, arguments +# Split the line into its component parts +def parse_line(line): + def read_byte(): + # Read one byte and advance the index + nonlocal line, index + + if eol(): + raise LineParsingError + + byte = line[index] + index += 1 + + return byte + + def peek_byte(): + # Look at current byte, don't advance index + nonlocal line, index + + if eol(): + raise LineParsingError + + return line[index] + + def eol(): + # Test if we've reached the end of the line + nonlocal line, index + return index >= len(line) + + def skip_space(): + # Skip until we run into a non-space character or eol. + while not eol() and peek_byte() == ord(' '): + read_byte() + + def read_until_space(): + nonlocal line, index + + if eol(): + raise LineParsingError + + # Try to find a space + until = line[index:].find(b' ') + + if until == -1: + # Space not found, read until end of line + until = len(line) + else: + # Space found, add current index to it to get right index + until += index + + # Slice line upto the point of next space / end and update index + data = line[index:until] + index = until + + return data + + def read_until_end(): + nonlocal line, index + + if eol(): + raise LineParsingError + + # Read all of the data, and make index point to eol + data = line[index:] + index = len(line) + + return data + + index = 0 + + prefix = None + command = None + arguments = [] + + if peek_byte() == ord(':'): + read_byte() + prefix = read_until_space() + + skip_space() + + command = read_until_space() + + skip_space() + + while not eol(): + if peek_byte() == ord(':'): + read_byte() + argument = read_until_end() + else: + argument = read_until_space() + + arguments.append(argument) + + skip_space() + + return prefix, command, arguments + +def handle_line(line, *, irc): + try: + prefix, command, arguments = parse_line(line) + except LineParsingError: + irc.error("Cannot parse line" + line.decode(encoding = 'utf-8', errors = 'replace')) + + # TODO: handle line