diff --git a/sopel/bot.py b/sopel/bot.py index 016488f802..87e74190d9 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -76,8 +76,12 @@ def __init__(self, config, daemon=False): """ self.acivity = {} - self.server_capabilities = set() - """A set containing the IRCv3 capabilities that the server supports. + self.server_capabilities = {} + """A dict mapping supported IRCv3 capabilities to their options. + + For example, if the server specifies the capability ``sasl=EXTERNAL``, + it will be here as ``{"sasl": "EXTERNAL"}``. Capabilities specified + without any options will have ``None`` as the value. For servers that do not support IRCv3, this will be an empty set.""" self.enabled_capabilities = set() @@ -86,8 +90,9 @@ def __init__(self, config, daemon=False): """A dictionary of capability requests Maps the capability name to a list of tuples of the prefix ('-', '=', - or ''), the name of the requesting module, and the function to call if - the request is rejected.""" + or ''), the name of the requesting module, the function to call if the + the request is rejected, and the argument to the capability (or None). + """ self.privileges = dict() """A dictionary of channels to their users and privilege levels @@ -356,7 +361,7 @@ def _shutdown(self): ) ) - def cap_req(self, module_name, capability, failure_callback): + def cap_req(self, module_name, capability, arg=None, failure_callback=None): """Tell Sopel to request a capability when it starts. By prefixing the capability with `-`, it will be ensured that the @@ -379,21 +384,27 @@ def cap_req(self, module_name, capability, failure_callback): request, the `failure_callback` function will be called, if provided. The arguments will be a `Sopel` object, and the capability which was rejected. This can be used to disable callables which rely on the - capability. + capability. In future versions + If ``arg`` is given, and does not exactly match what the server + provides or what other modules have requested for that capability, it is + considered a conflict. """ # TODO raise better exceptions cap = capability[1:] prefix = capability[0] + entry = self._cap_reqs.get(cap, []) + if any((ent[3] != arg for ent in entry)): + raise Exception('Capability conflict') + if prefix == '-': if self.connection_registered and cap in self.enabled_capabilities: raise Exception('Can not change capabilities after server ' 'connection has been completed.') - entry = self._cap_reqs.get(cap, []) if any((ent[0] != '-' for ent in entry)): raise Exception('Capability conflict') - entry.append((prefix, module_name, failure_callback)) + entry.append((prefix, module_name, failure_callback, arg)) self._cap_reqs[cap] = entry else: if prefix != '=': @@ -403,10 +414,9 @@ def cap_req(self, module_name, capability, failure_callback): self.enabled_capabilities): raise Exception('Can not change capabilities after server ' 'connection has been completed.') - entry = self._cap_reqs.get(cap, []) # Non-mandatory will callback at the same time as if the server # rejected it. if any((ent[0] == '-' for ent in entry)) and prefix == '=': raise Exception('Capability conflict') - entry.append((prefix, module_name, failure_callback)) + entry.append((prefix, module_name, failure_callback, arg)) self._cap_reqs[cap] = entry diff --git a/sopel/coretasks.py b/sopel/coretasks.py index dd2a81a2a6..4153dd8595 100644 --- a/sopel/coretasks.py +++ b/sopel/coretasks.py @@ -30,6 +30,8 @@ LOGGER = get_logger(__name__) +batched_caps = {} + def auth_after_register(bot): """Do NickServ/AuthServ auth""" @@ -324,14 +326,26 @@ def recieve_cap_ls_reply(bot, trigger): # We're too late to do SASL, and we don't want to send CAP END before # the module has done what it needs to, so just return return - bot.server_capabilities = set(trigger.split(' ')) + + for cap in trigger.split(): + c = cap.split('=') + if len(c) == 2: + batched_caps[c[0]] = c[1] + else: + batched_caps[c[0]] = None + + # Not the last in a multi-line reply. First two args are * and LS. + if trigger.args[2] == '*': + return + + bot.server_capabilities = batched_caps # If some other module requests it, we don't need to add another request. # If some other module prohibits it, we shouldn't request it. if 'multi-prefix' not in bot._cap_reqs: # Whether or not the server supports multi-prefix doesn't change how we # parse it, so we don't need to worry if it fails. - bot._cap_reqs['multi-prefix'] = (['', 'coretasks', None],) + bot._cap_reqs['multi-prefix'] = (['', 'coretasks', None, None],) for cap, reqs in iteritems(bot._cap_reqs): # At this point, we know mandatory and prohibited don't co-exist, but @@ -348,9 +362,12 @@ def recieve_cap_ls_reply(bot, trigger): if prefix != '=' or cap in bot.server_capabilities: # REQs fail as a whole, so we send them one capability at a time bot.write(('CAP', 'REQ', entry[0] + cap)) - elif req[2]: - # Server is going to fail on it, so we call the failure function - req[2](bot, entry[0] + cap) + # If it's required but not in server caps, we need to call all the + # callbacks + else: + for entry in reqs: + if entry[2] and entry[0] == '=': + entry[2](bot, entry[0] + cap) # If we want to do SASL, we have to wait before we can send CAP END. So if # we are, wait on 903 (SASL successful) to send it. diff --git a/sopel/irc.py b/sopel/irc.py index 7f64e47999..7d856337ef 100644 --- a/sopel/irc.py +++ b/sopel/irc.py @@ -273,7 +273,7 @@ def handle_connect(self): # Request list of server capabilities. IRCv3 servers will respond with # CAP * LS (which we handle in coretasks). v2 servers will respond with # 421 Unknown command, which we'll ignore - self.write(('CAP', 'LS')) + self.write(('CAP', 'LS', '302')) if self.config.core.auth_method == 'server': password = self.config.core.auth_password