Skip to content

Commit

Permalink
Add cap-notify support
Browse files Browse the repository at this point in the history
  • Loading branch information
embolalia authored and fatalis committed Jan 7, 2016
1 parent 6b80092 commit 20a76d7
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 35 deletions.
41 changes: 28 additions & 13 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 != '=':
Expand All @@ -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
72 changes: 50 additions & 22 deletions sopel/coretasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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.
Expand Down

0 comments on commit 20a76d7

Please sign in to comment.