Skip to content

Commit fcd897b

Browse files
committed
Don't rely on colons in the irc parser
1 parent 08bfa9d commit fcd897b

File tree

5 files changed

+440
-107
lines changed

5 files changed

+440
-107
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Fix grammatical error in food sentence (beer)
2121
- Update youtube plugin to use proper contentRating API
2222
- Update mylife.py for website changes
23+
- Fixed handling of colons in core IRC parser
2324
### Removed
2425
- twitch.py removed due to outdated API and lack of maintainer
2526

cloudbot/clients/irc.py

Lines changed: 122 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import traceback
88
from functools import partial
99
from pathlib import Path
10+
from typing import Mapping, Optional
1011

1112
from irclib.parser import Message
1213

@@ -34,6 +35,36 @@ def irc_clean(dirty):
3435
"NOTICE": EventType.notice
3536
}
3637

38+
content_params = {
39+
"PRIVMSG": 1,
40+
"NOTICE": 1,
41+
"PART": 1,
42+
"KICK": 2,
43+
"TOPIC": 1,
44+
"NICK": 0,
45+
"QUIT": 0,
46+
}
47+
48+
chan_params = {
49+
"PRIVMSG": 0,
50+
"NOTICE": 0,
51+
"JOIN": 0,
52+
"PART": 0,
53+
"TOPIC": 0,
54+
"MODE": 0,
55+
"KICK": 0,
56+
"INVITE": 1,
57+
"353": 2,
58+
"366": 1,
59+
"324": 1,
60+
}
61+
62+
target_params = {
63+
"KICK": 1,
64+
"INVITE": 0,
65+
"MODE": 0,
66+
}
67+
3768

3869
def decode(bytestring):
3970
"""
@@ -47,6 +78,15 @@ def decode(bytestring):
4778
return bytestring.decode('utf-8', errors='ignore')
4879

4980

81+
def _get_param(msg: Message, index_map: Mapping[str, int]) -> Optional[str]:
82+
if msg.command in index_map:
83+
idx = index_map[msg.command]
84+
if idx < len(msg.parameters):
85+
return msg.parameters[idx]
86+
87+
return None
88+
89+
5090
@client("irc")
5191
class IrcClient(Client):
5292
"""
@@ -417,114 +457,97 @@ def data_received(self, data):
417457
line = decode(line_data)
418458

419459
try:
420-
message = Message.parse(line)
460+
event = self.parse_line(line)
421461
except Exception:
422462
logger.exception(
423463
"[%s] Error occurred while parsing IRC line '%s' from %s",
424464
self.conn.name, line, self.conn.describe_server()
425465
)
426-
continue
427-
428-
command = message.command
429-
command_params = message.parameters
430-
431-
# Reply to pings immediately
432-
433-
if command == "PING":
434-
self.conn.send("PONG " + command_params[-1], log=False)
435-
436-
# Parse the command and params
437-
438-
# Content
439-
if command_params.has_trail:
440-
content_raw = command_params[-1]
441-
content = irc_clean(content_raw)
442466
else:
443-
content_raw = None
444-
content = None
445-
446-
# Event type
447-
event_type = irc_command_to_event_type.get(
448-
command, EventType.other
449-
)
467+
# handle the message, async
468+
async_util.wrap_future(self.bot.process(event), loop=self.loop)
469+
470+
def parse_line(self, line: str) -> Event:
471+
message = Message.parse(line)
472+
command = message.command
473+
command_params = message.parameters
474+
475+
# Reply to pings immediately
476+
if command == "PING":
477+
self.conn.send("PONG " + command_params[-1], log=False)
478+
479+
# Parse the command and params
480+
# Content
481+
content_raw = _get_param(message, content_params)
482+
if content_raw is not None:
483+
content = irc_clean(content_raw)
484+
else:
485+
content = None
450486

451-
# Target (for KICK, INVITE)
452-
if event_type is EventType.kick:
453-
target = command_params[1]
454-
elif command in ("INVITE", "MODE"):
455-
target = command_params[0]
456-
else:
457-
# TODO: Find more commands which give a target
458-
target = None
459-
460-
# Parse for CTCP
461-
if event_type is EventType.message and content_raw.startswith("\x01"):
462-
possible_ctcp = content_raw[1:]
463-
if content_raw.endswith('\x01'):
464-
possible_ctcp = possible_ctcp[:-1]
465-
466-
if '\x01' in possible_ctcp:
467-
logger.debug(
468-
"[%s] Invalid CTCP message received, "
469-
"treating it as a mornal message",
470-
self.conn.name
471-
)
472-
ctcp_text = None
473-
else:
474-
ctcp_text = possible_ctcp
475-
ctcp_text_split = ctcp_text.split(None, 1)
476-
if ctcp_text_split[0] == "ACTION":
477-
# this is a CTCP ACTION, set event_type and content accordingly
478-
event_type = EventType.action
479-
content = irc_clean(ctcp_text_split[1])
480-
else:
481-
# this shouldn't be considered a regular message
482-
event_type = EventType.other
483-
else:
487+
# Event type
488+
event_type = irc_command_to_event_type.get(
489+
command, EventType.other
490+
)
491+
target = _get_param(message, target_params)
492+
493+
# Parse for CTCP
494+
if event_type is EventType.message and content_raw.startswith("\x01"):
495+
possible_ctcp = content_raw[1:]
496+
if content_raw.endswith('\x01'):
497+
possible_ctcp = possible_ctcp[:-1]
498+
499+
if '\x01' in possible_ctcp:
500+
logger.debug(
501+
"[%s] Invalid CTCP message received, "
502+
"treating it as a mornal message",
503+
self.conn.name
504+
)
484505
ctcp_text = None
485-
486-
# Channel
487-
channel = None
488-
if command_params:
489-
if command in ["NOTICE", "PRIVMSG", "KICK", "JOIN", "PART", "MODE"]:
490-
channel = command_params[0]
491-
elif command == "INVITE":
492-
channel = command_params[1]
493-
elif len(command_params) > 2 or not (command_params.has_trail and len(command_params) == 1):
494-
channel = command_params[0]
495-
496-
prefix = message.prefix
497-
498-
if prefix is None:
499-
nick = None
500-
user = None
501-
host = None
502-
mask = None
503506
else:
504-
nick = prefix.nick
505-
user = prefix.user
506-
host = prefix.host
507-
mask = prefix.mask
508-
509-
if channel:
510-
# TODO Migrate plugins to accept the original case of the channel
511-
channel = channel.lower()
512-
513-
channel = channel.split()[0] # Just in case there is more data
514-
515-
if channel == self.conn.nick.lower():
516-
channel = nick.lower()
517-
518-
# Set up parsed message
519-
# TODO: Do we really want to send the raw `prefix` and `command_params` here?
520-
event = Event(
521-
bot=self.bot, conn=self.conn, event_type=event_type, content_raw=content_raw, content=content,
522-
target=target, channel=channel, nick=nick, user=user, host=host, mask=mask, irc_raw=line,
523-
irc_prefix=mask, irc_command=command, irc_paramlist=command_params, irc_ctcp_text=ctcp_text
524-
)
525-
526-
# handle the message, async
527-
async_util.wrap_future(self.bot.process(event), loop=self.loop)
507+
ctcp_text = possible_ctcp
508+
ctcp_text_split = ctcp_text.split(None, 1)
509+
if ctcp_text_split[0] == "ACTION":
510+
# this is a CTCP ACTION, set event_type and content accordingly
511+
event_type = EventType.action
512+
content = irc_clean(ctcp_text_split[1])
513+
else:
514+
# this shouldn't be considered a regular message
515+
event_type = EventType.other
516+
else:
517+
ctcp_text = None
518+
519+
# Channel
520+
channel = _get_param(message, chan_params)
521+
prefix = message.prefix
522+
if prefix is None:
523+
nick = None
524+
user = None
525+
host = None
526+
mask = None
527+
else:
528+
nick = prefix.nick
529+
user = prefix.user
530+
host = prefix.host
531+
mask = prefix.mask
532+
533+
if channel:
534+
# TODO Migrate plugins to accept the original case of the channel
535+
channel = channel.lower()
536+
537+
channel = channel.split()[0] # Just in case there is more data
538+
539+
# Channel for a PM is the sending user
540+
if channel == self.conn.nick.lower():
541+
channel = nick.lower()
542+
543+
# Set up parsed message
544+
# TODO: Do we really want to send the raw `prefix` and `command_params` here?
545+
event = Event(
546+
bot=self.bot, conn=self.conn, event_type=event_type, content_raw=content_raw, content=content,
547+
target=target, channel=channel, nick=nick, user=user, host=host, mask=mask, irc_raw=line,
548+
irc_prefix=mask, irc_command=command, irc_paramlist=command_params, irc_ctcp_text=ctcp_text
549+
)
550+
return event
528551

529552
@property
530553
def connected(self):

cloudbot/event.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import enum
33
import logging
44
from functools import partial
5+
from typing import Any, Iterator, Mapping
56

67
from irclib.parser import Message
78

@@ -20,7 +21,7 @@ class EventType(enum.Enum):
2021
other = 6
2122

2223

23-
class Event:
24+
class Event(Mapping[str, Any]):
2425
"""
2526
:type bot: cloudbot.bot.CloudBot
2627
:type conn: cloudbot.client.Client
@@ -38,7 +39,7 @@ class Event:
3839
:type irc_raw: str
3940
:type irc_prefix: str
4041
:type irc_command: str
41-
:type irc_paramlist: str
42+
:type irc_paramlist: list[str]
4243
:type irc_ctcp_text: str
4344
"""
4445

@@ -135,6 +136,18 @@ def __init__(self, *, bot=None, hook=None, conn=None, base_event=None, event_typ
135136
self.irc_paramlist = irc_paramlist
136137
self.irc_ctcp_text = irc_ctcp_text
137138

139+
def __len__(self) -> int:
140+
return len(self.__dict__)
141+
142+
def __iter__(self) -> Iterator[str]:
143+
return iter(self.__dict__)
144+
145+
def __getitem__(self, item: str) -> Any:
146+
try:
147+
return getattr(self, item)
148+
except AttributeError:
149+
raise KeyError(item)
150+
138151
async def prepare(self):
139152
"""
140153
Initializes this event to be run through it's hook
@@ -383,12 +396,6 @@ def is_nick_valid(self, nick):
383396
"""
384397
return self.conn.is_nick_valid(nick)
385398

386-
def __getitem__(self, item):
387-
try:
388-
return getattr(self, item)
389-
except AttributeError:
390-
raise KeyError(item)
391-
392399

393400
class CommandEvent(Event):
394401
"""

0 commit comments

Comments
 (0)