Skip to content

Commit

Permalink
Add IRCv3.2 CAP negotiation
Browse files Browse the repository at this point in the history
No cap-notify yet, because that's going to be a pain and all indications
at the moment are that nobody actually uses caps at all.
  • Loading branch information
embolalia committed Aug 29, 2015
1 parent df0b723 commit e4a68ee
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 16 deletions.
30 changes: 20 additions & 10 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 != '=':
Expand All @@ -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
27 changes: 22 additions & 5 deletions sopel/coretasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

LOGGER = get_logger(__name__)

batched_caps = {}


def auth_after_register(bot):
"""Do NickServ/AuthServ auth"""
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion sopel/irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit e4a68ee

Please sign in to comment.