Skip to content
This repository has been archived by the owner on Oct 23, 2019. It is now read-only.

Commit

Permalink
Split up parsing and message handling in clients/irc.py
Browse files Browse the repository at this point in the history
  • Loading branch information
linuxdaemon committed Mar 9, 2019
1 parent 5a55b79 commit 976612d
Showing 1 changed file with 157 additions and 104 deletions.
261 changes: 157 additions & 104 deletions cloudbot/clients/irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ def is_nick_valid(self, nick):
return bool(irc_nick_re.fullmatch(nick))


class LineParseError(ValueError):
def __init__(self, line):
super().__init__("Unable to parse: {!r}".format(line))
self.line = line


class _IrcProtocol(asyncio.Protocol):
"""
:type loop: asyncio.events.AbstractEventLoop
Expand Down Expand Up @@ -386,122 +392,169 @@ async def send(self, line, log=True):

self._transport.write(line)

def data_received(self, data):
def has_line(self):
return b'\r\n' in self._input_buffer

def get_line(self):
line_data, self._input_buffer = self._input_buffer.split(b'\r\n', 1)
return line_data

def get_decoded_line(self):
return decode(self.get_line())

def get_parsed_line(self):
line = self.get_decoded_line()
try:
return Message.parse(line)
except Exception as e:
raise LineParseError(line) from e

def feed(self, data):
self._input_buffer += data

while b"\r\n" in self._input_buffer:
line_data, self._input_buffer = self._input_buffer.split(b"\r\n", 1)
line = decode(line_data)
def parse_ctcp(self, text: str):
if not text.startswith('\x01'):
return None

try:
message = Message.parse(line)
except Exception: # pylint: disable=broad-except
logger.exception(
"[%s] Error occurred while parsing IRC line '%s' from %s",
self.conn.name, line, self.conn.describe_server()
)
continue
if text.endswith('\x01'):
text = text[:-1]

command = message.command
command_params = message.parameters
if '\x01' in text:
logger.debug(
"[%s] Invalid CTCP message received, "
"treating it as a normal message",
self.conn.name
)
return None

# Reply to pings immediately
ctcp_cmd, _, param = text.partition(' ')
return ctcp_cmd.upper(), param.strip()

if command == "PING":
self.conn.send("PONG " + command_params[-1], log=False)
@staticmethod
def get_channel(message: Message):
command_params = message.parameters
command = message.command

# Parse the command and params
if command_params:
if command in ["NOTICE", "PRIVMSG", "KICK", "JOIN", "PART", "MODE"]:
return command_params[0]

# Content
if command_params.has_trail:
content_raw = command_params[-1]
content = irc_clean(content_raw)
else:
content_raw = None
content = None
if command == "INVITE":
return command_params[1]

# Event type
event_type = irc_command_to_event_type.get(
command, EventType.other
)
if len(command_params) > 2 or not (command_params.has_trail and len(command_params) == 1):
return command_params[0]

# Target (for KICK, INVITE)
if event_type is EventType.kick:
target = command_params[1]
elif command in ("INVITE", "MODE"):
target = command_params[0]
else:
# TODO: Find more commands which give a target
target = None

# Parse for CTCP
if event_type is EventType.message and content_raw.startswith("\x01"):
possible_ctcp = content_raw[1:]
if content_raw.endswith('\x01'):
possible_ctcp = possible_ctcp[:-1]

if '\x01' in possible_ctcp:
logger.debug(
"[%s] Invalid CTCP message received, "
"treating it as a mornal message",
self.conn.name
)
ctcp_text = None
return None

@staticmethod
def get_target(message, event_type):
command_params = message.parameters
command = message.command
# Target (for KICK, INVITE)
if event_type is EventType.kick:
return command_params[1]

if command in ("INVITE", "MODE"):
return command_params[0]

# TODO: Find more commands which give a target
return None

def handle_line(self, message: Message):
command = message.command
command_params = message.parameters

# Reply to pings immediately

if command == "PING":
self.conn.send("PONG " + command_params[-1], log=False)

# Parse the command and params

# Content
if command_params.has_trail:
content_raw = command_params[-1]
content = irc_clean(content_raw)
else:
content_raw = None
content = None

# Event type
event_type = irc_command_to_event_type.get(
command, EventType.other
)

target = self.get_target(message, event_type)

ctcp_cmd = ctcp_param = ctcp_text = None
# Parse for CTCP
if event_type is EventType.message:
ctcp = self.parse_ctcp(command_params[-1])
if ctcp:
ctcp_cmd, ctcp_param = ctcp
if ctcp_cmd == "ACTION":
# this is a CTCP ACTION, set event_type and content accordingly
event_type = EventType.action
content = irc_clean(ctcp_param)
else:
ctcp_text = possible_ctcp
ctcp_text_split = ctcp_text.split(None, 1)
if ctcp_text_split[0] == "ACTION":
# this is a CTCP ACTION, set event_type and content accordingly
event_type = EventType.action
content = irc_clean(ctcp_text_split[1])
else:
# this shouldn't be considered a regular message
event_type = EventType.other
else:
ctcp_text = None

# Channel
channel = None
if command_params:
if command in ["NOTICE", "PRIVMSG", "KICK", "JOIN", "PART", "MODE"]:
channel = command_params[0]
elif command == "INVITE":
channel = command_params[1]
elif len(command_params) > 2 or not (command_params.has_trail and len(command_params) == 1):
channel = command_params[0]

prefix = message.prefix

if prefix is None:
nick = None
user = None
host = None
mask = None
else:
nick = prefix.nick
user = prefix.user
host = prefix.host
mask = prefix.mask

if channel:
# TODO Migrate plugins to accept the original case of the channel
channel = channel.lower()

channel = channel.split()[0] # Just in case there is more data

if channel == self.conn.nick.lower():
channel = nick.lower()

# Set up parsed message
# TODO: Do we really want to send the raw `prefix` and `command_params` here?
event = Event(
bot=self.bot, conn=self.conn, event_type=event_type, content_raw=content_raw, content=content,
target=target, channel=channel, nick=nick, user=user, host=host, mask=mask, irc_raw=line,
irc_prefix=mask, irc_command=command, irc_paramlist=command_params, irc_ctcp_text=ctcp_text
)
# this shouldn't be considered a regular message
event_type = EventType.other

if ctcp_cmd:
ctcp_text = ' '.join((ctcp_cmd, ctcp_param))

# Channel
channel = self.get_channel(message)
prefix = message.prefix

if prefix is None:
nick = None
user = None
host = None
mask = None
else:
nick = prefix.nick
user = prefix.user
host = prefix.host
mask = prefix.mask

if channel:
# TODO Migrate plugins to accept the original case of the channel
channel = channel.lower()

channel = channel.split()[0] # Just in case there is more data

if channel == self.conn.nick.lower():
channel = nick.lower()

# Set up parsed message
# TODO: Do we really want to send the raw `prefix` and `command_params` here?
event = Event(
bot=self.bot, conn=self.conn, event_type=event_type, content_raw=content_raw, content=content,
target=target, channel=channel, nick=nick, user=user, host=host, mask=mask, irc_raw=str(message),
irc_prefix=mask, irc_command=command, irc_paramlist=command_params, irc_ctcp_text=ctcp_text
)

# handle the message, async
async_util.wrap_future(self.bot.process(event), loop=self.loop)

def data_received(self, data):
self.feed(data)

while self.has_line():
try:
message = self.get_parsed_line()
except LineParseError as e:
self.conn.admin_log(
"[{}] Error occurred while parsing "
"IRC line '{}' from {}".format(
self.conn.name, e.line, self.conn.describe_server()
)
)
continue

# handle the message, async
async_util.wrap_future(self.bot.process(event), loop=self.loop)
self.handle_line(message)

@property
def connected(self):
Expand Down

0 comments on commit 976612d

Please sign in to comment.