def test_param_construct(): msg = Message(None, None, "PRIVMSG", "#channel", "Message thing") assert str(msg) == "PRIVMSG #channel :Message thing" msg = Message(None, None, "PRIVMSG", ["#channel", "Message thing"]) assert str(msg) == "PRIVMSG #channel :Message thing" msg = Message(None, None, "PRIVMSG", ["#channel", ":Message thing"]) assert str(msg) == "PRIVMSG #channel ::Message thing" msg = Message(None, None, "PRIVMSG", [""]) assert str(msg) == "PRIVMSG :"
def test_line(): assert Message.parse("COMMAND") == "COMMAND" assert Message.parse("command") == "COMMAND" msg1 = Message.parse("PRIVMSG") assert msg1.command == "PRIVMSG" msg2 = Message.parse( "@test=data;test1=more\sdata :nick!user@host COMMAND arg1 arg2 :trailing text" ) assert msg2.prefix.host == "host"
def test_check_send_key(mock_db): conn = make_conn() db = mock_db.session() chan_key_db.table.create(mock_db.engine) msg = Message(None, None, "JOIN", ["#foo,#bar", "bing"]) assert chan_key_db.check_send_key(conn, msg, db) is msg assert conn.get_channel_key("#foo") == "bing" msg = Message(None, None, "PRIVMSG", ["#foo,#bar", "bing"]) assert chan_key_db.check_send_key(conn, msg, db) is msg msg = Message(None, None, "JOIN", ["#foo,#bar"]) assert chan_key_db.check_send_key(conn, msg, db) is msg assert conn.get_channel_key("#foo") == "bing"
def transcode_off(self, data: bytes) -> None: self._buff += data while b'\r\n' in self._buff: raw_line, self._buff = self._buff.split(b'\r\n', 1) message = Message.parse(raw_line) for trigger, func in self.handlers.values(): if trigger in (message.command, '*'): self.loop.create_task(func(self, message))
def test_trail(): """Ensure this parser does not have the same issue as https://github.com/hexchat/hexchat/issues/2271""" text = "COMMAND thing thing :thing" parsed = Message.parse(text) assert "COMMAND" == parsed.command for i in range(3): assert "thing" == parsed.parameters[i]
def data_received(self, data: bytes) -> None: """Called by the event loop when data has been read from the socket""" self._buff += data while b'\r\n' in self._buff: raw_line, self._buff = self._buff.split(b'\r\n', 1) message = Message.parse(raw_line) for trigger, func in self.handlers.values(): if trigger in (message.command, '*'): self.loop.create_task(func(self, message))
def cmd(self, command, *params): """ Sends a raw IRC command of type <command> with params <params> :param command: The IRC command to send :param params: The params to the IRC command :type command: str :type params: (str) """ params = list(map(str, params)) # turn the tuple of parameters into a list self.send(str(Message(None, None, command, params)))
def prepare_threaded(self): super().prepare_threaded() if "parsed_line" in self.hook.required_args: try: self.parsed_line = Message.parse(self.line) except Exception: logger.exception("Unable to parse line requested by hook %s", self.hook) self.parsed_line = None
def test_names_handling(): from plugins.core.server_info import handle_prefixes, handle_chan_modes from plugins.core.chan_track import on_join, on_part, on_kick, on_quit, on_names handlers = { "JOIN": on_join, "PART": on_part, "QUIT": on_quit, "KICK": on_kick, "353": on_names, "366": on_names, } chan_pos = { "JOIN": 0, "PART": 0, "KICK": 0, "353": 2, "366": 1, } bot = MagicMock() bot.loop = asyncio.get_event_loop() conn = MockConn(bot) serv_info = conn.memory["server_info"] handle_prefixes("(YohvV)!@%+-", serv_info) handle_chan_modes("IXZbegw,k,FHJLWdfjlx,ABCDKMNOPQRSTcimnprstuz", serv_info) for line in NAMES_MOCK_TRAFFIC: line = Message.parse(line) data = { "nick": line.prefix.nick, "user": line.prefix.ident, "host": line.prefix.host, "conn": conn, "irc_paramlist": line.parameters, "irc_command": line.command, "chan": None, "target": None, } if line.command in chan_pos: data["chan"] = line.parameters[chan_pos[line.command]] if line.command == "KICK": data["target"] = line.parameters[1] call_with_args(handlers[line.command], data)
def test_names_handling(): from plugins.core.server_info import handle_prefixes, handle_chan_modes from plugins.chan_track import on_join, on_part, on_kick, on_quit, on_names handlers = { 'JOIN': on_join, 'PART': on_part, 'QUIT': on_quit, 'KICK': on_kick, '353': on_names, '366': on_names, } chan_pos = { 'JOIN': 0, 'PART': 0, 'KICK': 0, '353': 2, '366': 1, } bot = MagicMock() bot.loop = asyncio.get_event_loop() conn = MockConn(bot) serv_info = conn.memory['server_info'] handle_prefixes('(YohvV)!@%+-', serv_info) handle_chan_modes('IXZbegw,k,FHJLWdfjlx,ABCDKMNOPQRSTcimnprstuz', serv_info) for line in NAMES_MOCK_TRAFFIC: line = Message.parse(line) data = { 'nick': line.prefix.nick, 'user': line.prefix.ident, 'host': line.prefix.host, 'conn': conn, 'irc_paramlist': line.parameters, 'irc_command': line.command, 'chan': None, 'target': None, } if line.command in chan_pos: data['chan'] = line.parameters[chan_pos[line.command]] if line.command == 'KICK': data['target'] = line.parameters[1] call_with_args(handlers[line.command], data)
def test_msg_join(data): """ Ensure that message building passes all tests from the irc-parser-tests library. """ atoms = data["atoms"] msg = Message( atoms.pop("tags", None), atoms.pop("source", None), atoms.pop("verb", None), atoms.pop("params", []), ) assert not atoms, "Not all atoms were handled" matches = data["matches"] assert str(msg) in matches
def test_msg_join(data): atoms = data['atoms'] msg = Message( atoms.pop('tags', None), atoms.pop('source', None), atoms.pop('verb', None), atoms.pop('params', []), ) assert not atoms, "Not all atoms were handled" matches = data['matches'] if len(matches) > 1: assert any(str(msg) == match for match in data['matches']) else: # With single matches, make it easier to debug assert str(msg) == matches[0]
def test_msg_split(data): msg = Message.parse(data['input']) atoms = data['atoms'].copy() # We store tags a bit differently than the test data expects, convert the format if msg.tags is not None: tags_dict = {name: tag.value for name, tag in msg.tags.items()} else: tags_dict = None assert tags_dict == atoms.pop('tags', None) prefix = None if msg.prefix is None else str(msg.prefix) assert prefix == atoms.pop('source', None) # Commands are case-insensitive assert String(msg.command, ASCII) == atoms.pop('verb', None) assert list(msg.parameters) == atoms.pop('params', []) # Make sure we handled everything assert not atoms
def test_msg_split(data): """Test splitting a message against the irc-parser-tests data""" msg = Message.parse(data["input"]) atoms = data["atoms"].copy() # We store tags a bit differently than the test data expects, convert the format if msg.tags is not None: tags_dict = {name: tag.value for name, tag in msg.tags.items()} else: tags_dict = None assert tags_dict == atoms.pop("tags", None) prefix = None if msg.prefix is None else str(msg.prefix) assert prefix == atoms.pop("source", None) # Commands are case-insensitive assert String(msg.command, ASCII) == atoms.pop("verb", None) assert list(msg.parameters) == atoms.pop("params", []) # Make sure we handled everything assert not atoms
class TestMessage: def test_parse_bytes(self): line = Message.parse(b"COMMAND some params :and stuff") assert line.command == 'COMMAND' assert line.parameters == ['some', 'params', 'and stuff'] @pytest.mark.parametrize('obj,text', [ (Message(None, None, None), ''), (Message(None, None, None, None), ''), (Message(None, None, None, []), ''), (Message(None, None, 'COMMAND'), 'COMMAND'), (Message(['a=b'], None, 'COMMAND'), '@a=b COMMAND'), (Message([MessageTag('a', 'b')], None, 'COMMAND'), '@a=b COMMAND'), (Message({'a': 'b'}, None, 'COMMAND'), '@a=b COMMAND'), (Message({'a': 'b'}, 'nick', 'COMMAND'), '@a=b :nick COMMAND'), (Message(None, ('nick', ), 'COMMAND'), ':nick COMMAND'), (Message(None, ('nick', 'user'), 'COMMAND'), ':nick!user COMMAND'), (Message(None, ('nick', 'user', 'host'), 'COMMAND'), ':nick!user@host COMMAND'), (Message({'a': 'b'}, 'nick', 'COMMAND', 'a', 'b'), '@a=b :nick COMMAND a b'), ]) def test_str(self, obj, text): assert str(obj) == text @pytest.mark.parametrize('tags,prefix,command,params', [ (None, None, None, None), ('some tag', None, 'COMMAND', ['param', '']), ]) def test_eq(self, tags, prefix, command, params): assert Message(tags, prefix, command, params) == Message(tags, prefix, command, params) @pytest.mark.parametrize('tags,prefix,command,params', [ (None, None, None, None), ('some tag', None, 'COMMAND', ['param', '']), ]) def test_ne(self, tags, prefix, command, params): assert not (Message(tags, prefix, command, params) != Message( tags, prefix, command, params)) @pytest.mark.parametrize('obj,other', [ (Message(None, None, None), 0), (Message(None, None, None), None), (Message(None, None, None), ()), ]) def test_no_cmp(self, obj, other): assert obj != other assert other != obj assert not (obj == other) assert not (other == obj) @pytest.mark.parametrize('obj', [ Message(None, None, 'COMMAND'), ]) def test_bool(self, obj): assert obj @pytest.mark.parametrize('obj', [ Message(None, None, None), Message(None, None, ''), Message(None, '', ''), Message('', '', ''), Message([], [], '', []), Message({}, [], '', []), Message(TagList(), Prefix(), '', ParamList()), ]) def test_bool_false(self, obj): assert not obj
def test_has_trail(text, has_trail): """Ensure that a message with trailing arguments is recorded as having a tril""" msg = Message.parse(text) assert msg.parameters.has_trail == has_trail
def test_ne(self, tags, prefix, command, params): """Test not-equals""" b = Message(tags, prefix, command, params) != Message( tags, prefix, command, params ) assert not b
def test_eq(self, tags, prefix, command, params): assert Message(tags, prefix, command, params) == Message(tags, prefix, command, params)
def test_parse_bytes(self): """Test parsing bytes""" line = Message.parse(b"COMMAND some params :and stuff") assert line.command == "COMMAND" assert line.parameters == ["some", "params", "and stuff"]
def test_parse_bytes(self): line = Message.parse(b"COMMAND some params :and stuff") assert line.command == 'COMMAND' assert line.parameters == ['some', 'params', 'and stuff']
def data_received(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) try: message = Message.parse(line) except Exception: logger.exception( "[%s] Error occurred while parsing IRC line '%s' from %s", self.conn.name, line, self.conn.describe_server() ) continue 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 (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 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 ) # handle the message, async async_util.wrap_future(self.bot.process(event), loop=self.loop)
def test_has_trail(text, has_trail): msg = Message.parse(text) assert msg.parameters.has_trail == has_trail
def test_eq(self, tags, prefix, command, params): """Test equals""" assert Message(tags, prefix, command, params) == Message( tags, prefix, command, params )
def test_ne(self, tags, prefix, command, params): assert not (Message(tags, prefix, command, params) != Message( tags, prefix, command, params))
def parse_line(self, line: str) -> Event: message = Message.parse(line) 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 content_raw = _get_param(message, content_params) if content_raw is not None: content = irc_clean(content_raw) else: content = None # Event type event_type = irc_command_to_event_type.get(command, EventType.other) target = _get_param(message, target_params) # 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 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 = _get_param(message, chan_params) 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 # Channel for a PM is the sending user if channel == self.conn.nick.lower(): channel = nick.lower() else: # If the channel isn't set, it's the sending user/server channel = nick.lower() if nick else nick # 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, irc_tags=message.tags, ) return event
class TestMessage: """Test parsing an entire IRC message""" def test_parse_bytes(self): """Test parsing bytes""" line = Message.parse(b"COMMAND some params :and stuff") assert line.command == "COMMAND" assert line.parameters == ["some", "params", "and stuff"] @pytest.mark.parametrize( "obj,text", [ (Message(None, None, None), ""), (Message(None, None, None, None), ""), (Message(None, None, None, []), ""), (Message(None, None, "COMMAND"), "COMMAND"), (Message(["a=b"], None, "COMMAND"), "@a=b COMMAND"), (Message([MessageTag("a", "b")], None, "COMMAND"), "@a=b COMMAND"), (Message({"a": "b"}, None, "COMMAND"), "@a=b COMMAND"), (Message({"a": "b"}, "nick", "COMMAND"), "@a=b :nick COMMAND"), (Message(None, ("nick",), "COMMAND"), ":nick COMMAND"), (Message(None, ("nick", "user"), "COMMAND"), ":nick!user COMMAND"), ( Message(None, ("nick", "user", "host"), "COMMAND"), ":nick!user@host COMMAND", ), ( Message({"a": "b"}, "nick", "COMMAND", "a", "b"), "@a=b :nick COMMAND a b", ), ], ) def test_str(self, obj, text): """Test string conversion""" assert str(obj) == text @pytest.mark.parametrize( "tags,prefix,command,params", [ (None, None, None, None), ("some tag", None, "COMMAND", ["param", ""]), ], ) def test_eq(self, tags, prefix, command, params): """Test equals""" assert Message(tags, prefix, command, params) == Message( tags, prefix, command, params ) @pytest.mark.parametrize( "tags,prefix,command,params", [ (None, None, None, None), ("some tag", None, "COMMAND", ["param", ""]), ], ) def test_ne(self, tags, prefix, command, params): """Test not-equals""" b = Message(tags, prefix, command, params) != Message( tags, prefix, command, params ) assert not b @pytest.mark.parametrize( "obj,other", [ (Message(None, None, None), 0), (Message(None, None, None), None), (Message(None, None, None), ()), ], ) def test_no_cmp(self, obj, other): """Test Message.__ne__""" assert obj != other assert other != obj assert not obj == other assert not other == obj @pytest.mark.parametrize( "obj", [ Message(None, None, "COMMAND"), ], ) def test_bool(self, obj): """Test the cases where bool(Message) should return True""" assert obj @pytest.mark.parametrize( "obj", [ Message(None, None, None), Message(None, None, ""), Message(None, "", ""), Message("", "", ""), Message([], [], "", []), Message({}, [], "", []), Message(TagList(), Prefix(), "", ParamList()), ], ) def test_bool_false(self, obj): """Test all the cases where bool(Message) should return False""" assert not obj