diff --git a/pydle/client.py b/pydle/client.py index 66176aa..74a21a2 100644 --- a/pydle/client.py +++ b/pydle/client.py @@ -372,7 +372,7 @@ async def handle_forever(self): try: await self.rawmsg("PING", self.server_tag) data = await self.connection.recv(timeout=self.READ_TIMEOUT) - except asyncio.TimeoutError: + except (asyncio.TimeoutError, ConnectionResetError) as e: data = None if not data: diff --git a/pydle/features/ircv3/tags.py b/pydle/features/ircv3/tags.py index 46be61a..934ca5e 100644 --- a/pydle/features/ircv3/tags.py +++ b/pydle/features/ircv3/tags.py @@ -3,14 +3,24 @@ import pydle.client import pydle.protocol from pydle.features import rfc1459 +import re TAG_INDICATOR = '@' TAG_SEPARATOR = ';' TAG_VALUE_SEPARATOR = '=' TAGGED_MESSAGE_LENGTH_LIMIT = 1024 +TAG_CONVERSIONS = { + r"\:": ';', + r"\s": ' ', + r"\\": '\\', + r"\r": '\r', + r"\n": '\n' +} + class TaggedMessage(rfc1459.RFC1459Message): + def __init__(self, tags=None, **kw): super().__init__(**kw) self._kw['tags'] = tags @@ -53,6 +63,20 @@ def parse(cls, line, encoding=pydle.protocol.DEFAULT_ENCODING): else: tag = raw_tag value = True + # Parse escape sequences since IRC escapes != python escapes + + # convert known escapes first + for escape, replacement in TAG_CONVERSIONS.items(): + value = value.replace(escape, replacement) + + # convert other escape sequences based on the spec + pattern =re.compile(r"(\\[\s\S])+") + for match in pattern.finditer(value): + escape = match.group() + value = value.replace(escape, escape[1]) + + + # Finally: add constructed tag to the output object. tags[tag] = value # Parse rest of message. @@ -77,7 +101,10 @@ def construct(self, force=False): message = TAG_INDICATOR + TAG_SEPARATOR.join(raw_tags) + ' ' + message if len(message) > TAGGED_MESSAGE_LENGTH_LIMIT and not force: - raise protocol.ProtocolViolation('The constructed message is too long. ({len} > {maxlen})'.format(len=len(message), maxlen=TAGGED_MESSAGE_LENGTH_LIMIT), message=message) + raise protocol.ProtocolViolation( + 'The constructed message is too long. ({len} > {maxlen})'.format(len=len(message), + maxlen=TAGGED_MESSAGE_LENGTH_LIMIT), + message=message) return message diff --git a/pydle/features/rfc1459/client.py b/pydle/features/rfc1459/client.py index 077692e..f83957f 100644 --- a/pydle/features/rfc1459/client.py +++ b/pydle/features/rfc1459/client.py @@ -923,6 +923,9 @@ async def on_raw_353(self, message): safe_entry = entry.lstrip(''.join(self._nickname_prefixes.keys())) # Parse entry and update database. nick, metadata = self._parse_user(safe_entry) + if not nick: + # nonsense nickname + continue self._sync_user(nick, metadata) # Get prefixes. diff --git a/tests/test_ircv3.py b/tests/test_ircv3.py new file mode 100644 index 0000000..f99410f --- /dev/null +++ b/tests/test_ircv3.py @@ -0,0 +1,24 @@ +import pytest + +from pydle.features import ircv3 + +pytestmark = pytest.mark.unit + + +@pytest.mark.parametrize( + "payload, expected", + [ + ( + rb"@+example=raw+:=,escaped\:\s\\ :irc.example.com NOTICE #channel :Message", + {"+example": """raw+:=,escaped; \\"""} + ), + ( + rb"@+example=\foo\bar :irc.example.com NOTICE #channel :Message", + {"+example": "foobar"} + ), + ] +) +def test_tagged_message_escape_sequences(payload, expected): + message = ircv3.tags.TaggedMessage.parse(payload) + + assert message.tags == expected diff --git a/tox.ini b/tox.ini index 3ae6c91..5bd18aa 100644 --- a/tox.ini +++ b/tox.ini @@ -15,3 +15,4 @@ markers = slow: may take several seconds or more to complete. meta: tests the test suite itself. real: tests pydle against a real server. Requires PYDLE_TESTS_REAL_HOST and PYDLE_TESTS_REAL_PORT environment variables. + unit: unit tests