diff --git a/sopel/bot.py b/sopel/bot.py index 6611c69925..e81f92af29 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -18,6 +18,7 @@ from sopel import irc, logger, plugins, tools from sopel.db import SopelDB +from sopel.irc import modes import sopel.loader from sopel.module import NOLIMIT from sopel.plugins import jobs as plugin_jobs, rules as plugin_rules @@ -78,6 +79,9 @@ def __init__(self, config, daemon=False): For servers that do not support IRCv3, this will be an empty set. """ + self.modeparser = modes.ModeParser() + """A mode parser used to parse ``MODE`` messages and modestrings.""" + self.channels = tools.SopelIdentifierMemory() """A map of the channels that Sopel is in. diff --git a/sopel/coretasks.py b/sopel/coretasks.py index 3f54c5f734..57651b7376 100644 --- a/sopel/coretasks.py +++ b/sopel/coretasks.py @@ -33,7 +33,7 @@ from sopel import loader, plugin from sopel.config import ConfigurationError -from sopel.irc import isupport, modes as ircmodes +from sopel.irc import isupport from sopel.irc.utils import CapReq, MyInfo from sopel.tools import events, Identifier, SopelMemory, target, web @@ -342,6 +342,13 @@ def handle_isupport(bot, trigger): bot._isupport = bot._isupport.apply(**parameters) + # update bot's mode parser + if 'CHANMODES' in bot.isupport: + bot.modeparser.chanmodes = bot.isupport.CHANMODES + + if 'PREFIX' in bot.isupport: + bot.modeparser.privileges = set(bot.isupport.PREFIX.keys()) + # was BOT mode support status updated? if not botmode_support and 'BOT' in bot.isupport: # yes it was! set our mode unless the config overrides it @@ -541,15 +548,7 @@ def _parse_modes(bot, args, clear=False): LOGGER.debug( "The server sent a possibly malformed MODE message: %r", args) - privileges = MODE_PREFIXES - if 'PREFIX' in bot.isupport: - privileges = set(bot.isupport.PREFIX.keys()) - - modemessage = ircmodes.ModeParser( - bot.isupport.CHANMODES, - privileges=privileges, - ) - modeinfo = modemessage.parse_modestring(args[1], tuple(args[2:])) + modeinfo = bot.modeparser.parse_modestring(args[1], tuple(args[2:])) # set or update channel's modes modes = {} if clear else copy.deepcopy(channel.modes) @@ -608,7 +607,7 @@ def _parse_modes(bot, args, clear=False): LOGGER.warning( "Too many arguments received for MODE: args=%r chanmodes=%r", args, - modemessage.chanmodes, + bot.modeparser.chanmodes, ) LOGGER.info("Updated mode for channel: %s", channel.name) diff --git a/sopel/irc/modes.py b/sopel/irc/modes.py index 4a9f088981..aa7593e077 100755 --- a/sopel/irc/modes.py +++ b/sopel/irc/modes.py @@ -120,9 +120,21 @@ class ModeParser: 'A': tuple('beI'), 'B': tuple('k'), 'C': tuple('l'), - 'D': tuple('Oaimnqpsrt'), + 'D': tuple('Oimnpsrt'), } - """Default CHANMODES per :rfc:`2811`.""" + """Default CHANMODES per :rfc:`2811`. + + .. note:: + + Mode ``a`` has been removed from the default list, as it appears + to be a relic of the past and is more commonly used as a privilege. + + Mode ``q`` has been removed too, as it is communly used as a privilege. + + If a server is unhappy with these defaults, they should advertise + ``CHANMODES`` and ``PREFIX`` properly. + + """ def __init__( self, @@ -130,18 +142,18 @@ def __init__( type_params: Dict[str, ParamRequired] = DEFAULT_MODETYPE_PARAM_CONFIG, privileges: Set[str] = PRIVILEGES, ) -> None: - self.chanmodes: Dict[str, Tuple[str, ...]] = chanmodes + self.chanmodes: Dict[str, Tuple[str, ...]] = dict(chanmodes) """Map of mode types (``str``) to their lists of modes (``tuple``). This map should come from ``ISUPPORT``, usually through :attr:`bot.isupport.CHANMODES `. """ - self.type_params = type_params + self.type_params = dict(type_params) """Map of mode types (``str``) with their param requirements. This map defaults to :data:`DEFAULT_MODETYPE_PARAM_CONFIG`. """ - self.privileges = privileges + self.privileges = set(privileges) """Set of valid user privileges. This set should come from ``ISUPPORT``, usually through diff --git a/test/irc/test_irc_modes.py b/test/irc/test_irc_modes.py index b8eb866224..1f3abf81e9 100644 --- a/test/irc/test_irc_modes.py +++ b/test/irc/test_irc_modes.py @@ -177,15 +177,13 @@ def test_modemessage_get_mode_info_custom_privileges(): def test_modemessage_parse_modestring_default(): modeparser = ModeParser() result = modeparser.parse_modestring( - '+Oaimn-qpsrt+lk-beI' + '+Z', + '+Oimn-psrt+lk-beI' + '+Z', tuple('abcdef')) assert result.modes == ( ('D', 'O', ADDED, None), - ('D', 'a', ADDED, None), ('D', 'i', ADDED, None), ('D', 'm', ADDED, None), ('D', 'n', ADDED, None), - ('D', 'q', REMOVED, None), ('D', 'p', REMOVED, None), ('D', 's', REMOVED, None), ('D', 'r', REMOVED, None), diff --git a/test/test_coretasks.py b/test/test_coretasks.py index 09fc2e1565..442c72f139 100644 --- a/test/test_coretasks.py +++ b/test/test_coretasks.py @@ -62,6 +62,7 @@ def test_bot_mixed_mode_removal(mockbot, ircfactory): """ irc = ircfactory(mockbot) irc.bot._isupport = isupport.ISupport(chanmodes=("b", "", "", "m", tuple())) + irc.bot.modeparser.chanmodes = irc.bot.isupport.CHANMODES irc.channel_joined('#test', ['Uvoice', 'Uop']) irc.mode_set('#test', '+qao', ['Uvoice', 'Uvoice', 'Uvoice']) @@ -88,6 +89,7 @@ def test_bot_mixed_mode_types(mockbot, ircfactory): """ irc = ircfactory(mockbot) irc.bot._isupport = isupport.ISupport(chanmodes=("be", "", "", "mn", tuple())) + irc.bot.modeparser.chanmodes = irc.bot.isupport.CHANMODES irc.channel_joined('#test', [ 'Uvoice', 'Uop', 'Uadmin', 'Uvoice2', 'Uop2', 'Uadmin2']) irc.mode_set('#test', '+amovn', ['Uadmin', 'Uop', 'Uvoice']) @@ -111,6 +113,7 @@ def test_bot_unknown_mode(mockbot, ircfactory): """Ensure modes not in PREFIX or CHANMODES trigger a WHO.""" irc = ircfactory(mockbot) irc.bot._isupport = isupport.ISupport(chanmodes=("b", "", "", "mnt", tuple())) + irc.bot.modeparser.chanmodes = irc.bot.isupport.CHANMODES irc.channel_joined("#test", ["Alex", "Bob", "Cheryl"]) irc.mode_set("#test", "+te", ["Alex"]) @@ -124,6 +127,7 @@ def test_bot_unknown_priv_mode(mockbot, ircfactory): """Ensure modes in `mapping` but not PREFIX are treated as unknown.""" irc = ircfactory(mockbot) irc.bot._isupport = isupport.ISupport(prefix={"o": "@", "v": "+"}) + irc.bot.modeparser.privileges = set(irc.bot.isupport.PREFIX.keys()) irc.channel_joined("#test", ["Alex", "Bob", "Cheryl"]) irc.mode_set("#test", "+oh", ["Alex", "Bob"]) @@ -137,6 +141,7 @@ def test_bot_extra_mode_args(mockbot, ircfactory, caplog): """Test warning on extraneous MODE args.""" irc = ircfactory(mockbot) irc.bot._isupport = isupport.ISupport(chanmodes=("b", "k", "l", "mnt", tuple())) + irc.bot.modeparser.chanmodes = irc.bot.isupport.CHANMODES irc.channel_joined("#test", ["Alex", "Bob", "Cheryl"]) mode_msg = ":Sopel!bot@bot MODE #test +m nonsense" @@ -159,6 +164,7 @@ def test_handle_rpl_channelmodeis(mockbot, ircfactory): ]) irc = ircfactory(mockbot) irc.bot._isupport = isupport.ISupport(chanmodes=("b", "k", "l", "mnt", tuple())) + irc.bot.modeparser.chanmodes = irc.bot.isupport.CHANMODES irc.channel_joined("#test", ["Alex", "Bob", "Cheryl"]) mockbot.on_message(rpl_channelmodeis) @@ -172,6 +178,7 @@ def test_handle_rpl_channelmodeis_clear(mockbot, ircfactory): """Test RPL_CHANNELMODEIS events clearing previous modes""" irc = ircfactory(mockbot) irc.bot._isupport = isupport.ISupport(chanmodes=("b", "k", "l", "mnt", tuple())) + irc.bot.modeparser.chanmodes = irc.bot.isupport.CHANMODES irc.channel_joined("#test", ["Alex", "Bob", "Cheryl"]) rpl_base = ":mercury.libera.chat 324 TestName #test {modes}"