From 20a76d7aff70149c48180c08428558d9e36f0b38 Mon Sep 17 00:00:00 2001 From: Embolalia Date: Sat, 19 Dec 2015 18:20:30 -0500 Subject: [PATCH] Add cap-notify support See #971 --- sopel/bot.py | 41 +++++++++++++++++--------- sopel/coretasks.py | 72 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 78 insertions(+), 35 deletions(-) diff --git a/sopel/bot.py b/sopel/bot.py index 058e747221..3563f9250b 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -39,6 +39,18 @@ py3 = False +class _CapReq(object): + def __init__(self, prefix, module, failure=None, arg=None, success=None): + def nop(bot, cap): + pass + # TODO at some point, reorder those args to be sane + self.prefix = prefix + self.module = module + self.arg = arg + self.failure = failure or nop + self.success = success or nop + + class Sopel(irc.Bot): def __init__(self, config, daemon=False): irc.Bot.__init__(self, config) @@ -85,12 +97,7 @@ def __init__(self, config, daemon=False): self.enabled_capabilities = set() """A set containing the IRCv3 capabilities that the bot has enabled.""" self._cap_reqs = dict() - """A dictionary of capability requests - - Maps the capability name to a list of tuples of the prefix ('-', '=', - 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). - """ + """A dictionary of capability names to a list of requests""" self.privileges = dict() """A dictionary of channels to their users and privilege levels @@ -381,7 +388,8 @@ def _shutdown(self): ) ) - def cap_req(self, module_name, capability, arg=None, failure_callback=None): + def cap_req(self, module_name, capability, arg=None, failure_callback=None, + success_callback=None): """Tell Sopel to request a capability when it starts. By prefixing the capability with `-`, it will be ensured that the @@ -404,7 +412,12 @@ def cap_req(self, module_name, capability, arg=None, failure_callback=None): 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. In future versions + capability. It will be be called either if the server NAKs the request, + or if the server enabled it and later DELs it. + + The `success_callback` function will be called upon acknowledgement of + the capability from the server, whether during the initial capability + negotiation, or later. If ``arg`` is given, and does not exactly match what the server provides or what other modules have requested for that capability, it is @@ -415,16 +428,17 @@ def cap_req(self, module_name, capability, arg=None, failure_callback=None): prefix = capability[0] entry = self._cap_reqs.get(cap, []) - if any((ent[3] != arg for ent in entry)): + if any((ent.arg != 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.') - if any((ent[0] != '-' for ent in entry)): + if any((ent.prefix != '-' for ent in entry)): raise Exception('Capability conflict') - entry.append((prefix, module_name, failure_callback, arg)) + entry.append(_CapReq(prefix, module_name, failure_callback, arg, + success_callback)) self._cap_reqs[cap] = entry else: if prefix != '=': @@ -436,7 +450,8 @@ def cap_req(self, module_name, capability, arg=None, failure_callback=None): 'connection has been completed.') # Non-mandatory will callback at the same time as if the server # rejected it. - if any((ent[0] == '-' for ent in entry)) and prefix == '=': + if any((ent.prefix == '-' for ent in entry)) and prefix == '=': raise Exception('Capability conflict') - entry.append((prefix, module_name, failure_callback, arg)) + entry.append(_CapReq(prefix, module_name, failure_callback, arg, + success_callback)) self._cap_reqs[cap] = entry diff --git a/sopel/coretasks.py b/sopel/coretasks.py index 6be3251184..cd18d7afbe 100644 --- a/sopel/coretasks.py +++ b/sopel/coretasks.py @@ -22,6 +22,7 @@ import time import sopel import sopel.module +from sopel.bot import _CapReq from sopel.tools import Identifier, iteritems from sopel.tools.target import User, Channel import base64 @@ -353,25 +354,54 @@ def track_quit(bot, trigger): @sopel.module.priority('high') @sopel.module.unblockable def recieve_cap_list(bot, trigger): + cap = trigger.strip('-=~') # Server is listing capabilites if trigger.args[1] == 'LS': recieve_cap_ls_reply(bot, trigger) # Server denied CAP REQ elif trigger.args[1] == 'NAK': - entry = bot._cap_reqs.get(trigger, None) + entry = bot._cap_reqs.get(cap, None) # If it was requested with bot.cap_req if entry: for req in entry: # And that request was mandatory/prohibit, and a callback was # provided - if req[0] and req[2]: + if req.prefix and req.failure: # Call it. - req[2](bot, req[0] + trigger) - # Server is acknowledging SASL for us. + req.failure(bot, req.prefix + cap) + # Server is removing a capability + elif trigger.args[1] == 'DEL': + entry = bot._cap_reqs.get(cap, None) + # If it was requested with bot.cap_req + if entry: + for req in entry: + # And that request wasn't prohibit, and a callback was + # provided + if req.prefix != '-' and req.failure: + # Call it. + req.failure(bot, req.prefix + cap) + # Server is adding new capability + elif trigger.args[1] == 'NEW': + entry = bot._cap_reqs.get(cap, None) + # If it was requested with bot.cap_req + if entry: + for req in entry: + # And that request wasn't prohibit + if req.prefix != '-': + # Request it + bot.write(('CAP', 'REQ', req.prefix + cap)) + # Server is acknowledging a capability elif trigger.args[1] == 'ACK': - if (trigger.args[0] == bot.nick and 'sasl' in trigger.args[2]): - recieve_cap_ack_sasl(bot) - bot.enabled_capabilities.add(trigger.args[2].strip()) + caps = trigger.args[2].split() + for cap in caps: + cap.strip('-~= ') + bot.enabled_capabilities.add(cap) + entry = bot._cap_reqs.get(cap, []) + for req in entry: + if req.success: + req.success(bot, req.prefix + trigger) + if cap == 'sasl': # TODO why is this not done with bot.cap_req? + recieve_cap_ack_sasl(bot) def recieve_cap_ls_reply(bot, trigger): @@ -396,43 +426,41 @@ def recieve_cap_ls_reply(bot, trigger): # 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, None],) - if 'away-notify' not in bot._cap_reqs: - bot._cap_reqs['away-notify'] = (['', 'coretasks', None, None],) + core_caps = ['multi-prefix', 'away-notify', 'cap-notify'] + for cap in core_caps: + if cap not in bot._cap_reqs: + bot._cap_reqs[cap] = [_CapReq('', 'coretasks')] def acct_warn(bot, cap): LOGGER.info('Server does not support {}, or it conflicts with a custom ' 'module. User account validation unavailable or limited.' - .format(cap)) + .format(cap[1:])) auth_caps = ['account-notify', 'extended-join', 'account-tag'] for cap in auth_caps: if cap not in bot._cap_reqs: - bot._cap_reqs[cap] = (['', 'coretasks', None, acct_warn],) + bot._cap_reqs[cap] = [_CapReq('=', 'coretasks', acct_warn)] for cap, reqs in iteritems(bot._cap_reqs): # At this point, we know mandatory and prohibited don't co-exist, but # we need to call back for optionals if they're also prohibited prefix = '' for entry in reqs: - if prefix == '-' and entry[0] != '-': - entry[2](bot, entry[0] + cap) + if prefix == '-' and entry.prefix != '-': + entry.failure(bot, entry.prefix + cap) continue - if entry[0]: - prefix = entry[0] + if entry.prefix: + prefix = entry.prefix # It's not required, or it's supported, so we can request it 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)) + bot.write(('CAP', 'REQ', entry.prefix + 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 entry.failure and entry.prefix == '=': + entry.failure(bot, entry.prefix + 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.