Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions sopel/irc/isupport.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,89 @@ def _parse_prefix(value):
return tuple(zip(modes, prefixes))


class ClientTagDeny:
"""Storage for CLIENTTAGDENY ISUPPORT parameter values.

This class behaves more or less like a set, but is case-insensitive when
checking membership and stores all elements in lowercase.

If the special wildcard ``*`` is present, the :meth:`is_denied` method will
return ``True`` for any tag name, except if the explicit negation of that
tag name (``-tagname``) is also present.
"""
def __init__(self, iterable=None):
self._data = set(x.lower() for x in iterable) if iterable else set()

def __contains__(self, element: str) -> bool:
return element.lower() in self._data

def __eq__(self, other):
if not isinstance(other, ClientTagDeny):
return NotImplemented
return self._data == other._data

def __or__(self, other):
result = self.__class__(self)
result.update(other)
return result

def __ior__(self, other):
self.update(other)
return self

def add(self, element: str) -> None:
self._data.add(element.lower())

def remove(self, element: str) -> None:
self._data.remove(element.lower())

def discard(self, element: str) -> None:
self._data.discard(element.lower())

def clear(self):
self._data.clear()

def update(self, *others):
for other in others:
self._data.update(x.lower() for x in other)

def __iter__(self):
return iter(self._data)

def __len__(self):
return len(self._data)

def __repr__(self):
return f"{self.__class__.__name__}({list(self._data)!r})"

def is_denied(self, tagname: str) -> bool:
"""Check if the given ``tagname`` is denied.

:param tagname: the tag name to check
:return: ``True`` if the tag name is denied, ``False`` otherwise

Supports wildcard and explicit logic; checks are case-insensitive.
"""
tagname = tagname.lower()
if "*" in self._data and f"-{tagname}" not in self._data:
return True
elif tagname in self._data:
return True
return False


def _parse_clienttagdeny(value: str) -> ClientTagDeny:
return ClientTagDeny(value.split(','))


ISUPPORT_PARSERS = {
'AWAYLEN': int,
'CASEMAPPING': str,
'CHANLIMIT': _map_items(int),
'CHANMODES': _parse_chanmodes,
'CHANNELLEN': int,
'CHANTYPES': _optional(tuple),
'CLIENTTAGDENY': _optional(_parse_clienttagdeny, default=ClientTagDeny()),
'ELIST': _parse_elist,
'EXCEPTS': _optional(_single_character, default='e'),
'EXTBAN': _parse_extban,
Expand Down
60 changes: 60 additions & 0 deletions test/irc/test_irc_isupport.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,44 @@ def test_isupport_chanmodes_undefined():
assert set(instance.CHANMODES.values()) == {""}


def test_isupport_clienttagdeny():
# `assert`s MUST contain various mixed-case values to ensure expected
# case-insensitivity of checks
instance = isupport.ISupport(
clienttagdeny=isupport.ClientTagDeny(('*', '-react')),
)

assert 'ReacT' not in instance.CLIENTTAGDENY
assert '-rEAct' in instance.CLIENTTAGDENY
assert not instance.CLIENTTAGDENY.is_denied('ReAct')
assert 'fOObAr' not in instance.CLIENTTAGDENY
assert instance.CLIENTTAGDENY.is_denied('Foobar')

instance = isupport.ISupport(
clienttagdeny=isupport.ClientTagDeny(('react', 'foobar')),
)

assert 'rEaCt' in instance.CLIENTTAGDENY
assert instance.CLIENTTAGDENY.is_denied('ReAct')
assert 'fooBar' in instance.CLIENTTAGDENY
assert instance.CLIENTTAGDENY.is_denied('Foobar')
assert 'bAz' not in instance.CLIENTTAGDENY
assert not instance.CLIENTTAGDENY.is_denied('BaZ')


def test_isupport_clienttagdeny_empty():
instance = isupport.ISupport(clienttagdeny=isupport.ClientTagDeny())

assert 'react' not in instance.CLIENTTAGDENY
assert 'foobar' not in instance.CLIENTTAGDENY


def test_isupport_clienttagdeny_undefined():
instance = isupport.ISupport()

assert not hasattr(instance, 'CLIENTTAGDENY')


def test_isupport_maxlist():
instance = isupport.ISupport(maxlist=(('Z', 10), ('beI', 25)))
assert 'Z' in instance.MAXLIST
Expand Down Expand Up @@ -339,6 +377,7 @@ def test_parse_parameter_single_letter_raise(arg):
'-CHANMODES',
'-CHANNELLEN',
'-CHANTYPES',
'-CLIENTTAGDENY',
'-ELIST',
'-EXCEPTS',
'-EXTBAN',
Expand Down Expand Up @@ -415,6 +454,27 @@ def test_parse_parameter_chanmode_raise():
isupport.parse_parameter('CHANMODES=b,k,l')


def test_parse_parameter_clienttagdeny():
key, value = isupport.parse_parameter('CLIENTTAGDENY=react,foo,bar')

assert key == 'CLIENTTAGDENY'
assert value == isupport.ClientTagDeny(('react', 'foo', 'bar'))


def test_parse_parameter_clienttagdeny_empty():
key, value = isupport.parse_parameter('CLIENTTAGDENY=')

assert key == 'CLIENTTAGDENY'
assert value == isupport.ClientTagDeny(())


def test_parse_parameter_clienttagdeny_no_value():
key, value = isupport.parse_parameter('CLIENTTAGDENY')

assert key == 'CLIENTTAGDENY'
assert value == isupport.ClientTagDeny(())


def test_parse_parameter_elist():
key, value = isupport.parse_parameter('ELIST=C')

Expand Down