-
-
Notifications
You must be signed in to change notification settings - Fork 403
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
irc: add modes submodule to handle MODE messages
- Loading branch information
Showing
4 changed files
with
869 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
"""Mode management for IRC channels. | ||
.. seealso:: | ||
https://modern.ircdocs.horse/#mode-message | ||
.. versionadded:: 8.0 | ||
""" | ||
from __future__ import generator_stop | ||
|
||
from collections import namedtuple | ||
import logging | ||
from typing import Dict, Generator, List, Optional, Set, Tuple | ||
|
||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
PARAM_ALWAYS = 'always' | ||
PARAM_ADDED = 'added' | ||
PARAM_REMOVED = 'removed' | ||
PARAM_NEVER = 'never' | ||
DEFAULT_MODETYPE_PARAM_CONFIG = { | ||
'A': PARAM_ALWAYS, | ||
'B': PARAM_ALWAYS, | ||
'C': PARAM_ADDED, | ||
'D': PARAM_NEVER, | ||
} | ||
"""Default parameter requirements for mode types.""" | ||
|
||
|
||
class ModeException(Exception): | ||
"""Base exception class for mode management.""" | ||
pass | ||
|
||
|
||
class ModeTypeUnknown(ModeException): | ||
"""Exception when a mode's type is unknown or cannot be determined.""" | ||
def __init__(self, mode) -> None: | ||
super().__init__('Unknown type for mode %s' % mode) | ||
|
||
|
||
class ModeTypeImproperlyConfigured(ModeException): | ||
"""Exception when the mode's type management is not configured.""" | ||
def __init__(self, mode: str, letter: str) -> None: | ||
message = 'Type {mode} for mode {letter} is not properly configured.' | ||
super().__init__(message.format(mode=mode, letter=letter)) | ||
|
||
|
||
def _modes_is_added(modestring: str | ||
) -> Generator[Tuple[str, bool], None, None]: | ||
is_added = True | ||
for char in modestring: | ||
if char in '+-': | ||
is_added = char == '+' | ||
continue | ||
yield (char, is_added) | ||
|
||
|
||
ModeString = namedtuple('ModeString', [ | ||
'modes', | ||
'privileges', | ||
'ignored_modes', | ||
'leftover_params', | ||
]) | ||
|
||
|
||
class ModeMessage: | ||
"""Modestring parser for IRC's ``MODE`` messages for channel modes.""" | ||
PRIVILEGES: Set[str] = { | ||
"v", # VOICE | ||
"h", # HALFOP | ||
"o", # OP | ||
"a", # ADMIN | ||
"q", # OWNER | ||
"y", # OPER | ||
"Y", # OPER | ||
} | ||
"""Set of user privileges used by default.""" | ||
|
||
def __init__( | ||
self, | ||
chanmodes: Dict[str, Tuple[str, ...]], | ||
type_params: Dict[str, str] = DEFAULT_MODETYPE_PARAM_CONFIG, | ||
privileges: Set[str] = PRIVILEGES, | ||
) -> None: | ||
self.chanmodes: Dict[str, Tuple[str, ...]] = chanmodes | ||
"""Map of mode types (``str``) with their list of modes (``tuple``). | ||
This map should come from ``ISUPPORT``, usually through | ||
:attr:`bot.isupport.CHANMODES <sopel.irc.isupport.ISupport.CHANMODES>`. | ||
""" | ||
self.type_params = type_params | ||
"""Map of mode types (``str``) with their param requirements. | ||
This map default to :data:`DEFAULT_MODETYPE_PARAM_CONFIG`. | ||
""" | ||
self.privileges = privileges | ||
"""Set of valid user privileges. | ||
This set should come from ``ISUPPORT``, usually through | ||
:attr:`bot.isupport.PREFIX <sopel.irc.isupport.ISupport.PREFIX>`. | ||
If a server doesn't advertise its prefixes for user privileges, | ||
:attr:`PRIVILEGES` can be used as a default value. | ||
""" | ||
|
||
def get_mode_type(self, mode: str) -> str: | ||
"""Retrieve the type of ``mode``. | ||
:raise ModeTypeUnknown: if the mode's type cannont be determined | ||
:return: the mode's type as defined by :attr:`chanmodes` | ||
:: | ||
>>> mm = ModeMessage({'A': tuple('beI'), 'B': tuple('k')}, {}) | ||
>>> mm.get_mode_type('b') | ||
'A' | ||
>>> mm.get_mode_type('k') | ||
'B' | ||
This method will raise a :exc:`ModeTypeUnknown` if the mode is unknown, | ||
including the case where ``mode`` is actually a user privilege. | ||
(such as ``v``) | ||
""" | ||
for letter, modes in self.chanmodes.items(): | ||
if mode in modes: | ||
return letter | ||
raise ModeTypeUnknown(mode) | ||
|
||
def get_mode_info( | ||
self, | ||
mode: str, | ||
is_added: bool | ||
) -> Tuple[Optional[str], bool, bool]: | ||
"""Retrieve ``mode``'s information when added or removed. | ||
:raise ModeTypeUnknown: when the mode's type is unknown and isn't a | ||
user privilege | ||
:raise ModeTypeImproperlyConfigured: when the mode's type is known but | ||
there is no information for | ||
parameters (if and when they are | ||
required by the mode) | ||
:return: a tuple with three values: the mode type, if it requires a | ||
parameter, and if it's a channel mode or a user privilege | ||
:: | ||
>>> chanmodes = {'A': tuple('beI'), 'B': tuple('k')} | ||
>>> t_params = {'A': PARAM_ALWAYS, 'B': PARAM_ADDED} | ||
>>> mm = ModeMessage(chanmodes, t_params) | ||
>>> mm.get_mode_info('e', False) | ||
('A', True, False) | ||
>>> mm.get_mode_info('k', False) | ||
('B', False, False) | ||
>>> mm.get_mode_info('v', True) | ||
(None, True, True) | ||
.. note:: | ||
A user privilege ``mode`` doesn't have a type so the first value | ||
returned will be ``None`` in that case. | ||
""" | ||
try: | ||
letter = self.get_mode_type(mode) | ||
except ModeTypeUnknown: | ||
if mode in self.privileges: | ||
# a user privilege doesn't have a type | ||
return None, True, True | ||
# not a user privilege: re-raise error | ||
raise | ||
|
||
if letter not in self.type_params: | ||
# we don't know how to handle this type of mode | ||
raise ModeTypeImproperlyConfigured(mode, letter) | ||
|
||
type_param = self.type_params[letter] | ||
return letter, not type_param == PARAM_NEVER and ( | ||
type_param == PARAM_ALWAYS | ||
or type_param == PARAM_ADDED and is_added | ||
or type_param == PARAM_REMOVED and not is_added | ||
), False | ||
|
||
def parse_modestring( | ||
self, | ||
modestring: str, | ||
params: Tuple[str, ...] | ||
) -> ModeString: | ||
"""Parse a ``modestring`` for a channel with its ``params``. | ||
:return: the parsed and validated information for that ``modestring`` | ||
""" | ||
imodes = iter(_modes_is_added(modestring)) | ||
iparams = iter(params) | ||
modes: List = [] | ||
privileges: List = [] | ||
for mode, is_added in imodes: | ||
param = None | ||
try: | ||
letter, required, is_priv = self.get_mode_info(mode, is_added) | ||
if required: | ||
try: | ||
param = next(iparams) | ||
except StopIteration: | ||
# Not enough parameters: we have to stop here | ||
return ModeString( | ||
tuple(modes), | ||
tuple(privileges), | ||
((mode, is_added),) + tuple(imodes), | ||
tuple(), | ||
) | ||
except ModeException as modeerror: | ||
LOGGER.debug( | ||
'Invalid modestring: %r; error: %s', | ||
modestring, | ||
modeerror, | ||
) | ||
return ModeString( | ||
tuple(modes), | ||
tuple(privileges), | ||
((mode, is_added),) + tuple(imodes), | ||
tuple(iparams), | ||
) | ||
|
||
if is_priv: | ||
privileges.append((mode, is_added, param)) | ||
else: | ||
modes.append((letter, mode, is_added, param)) | ||
|
||
return ModeString( | ||
tuple(modes), | ||
tuple(privileges), | ||
tuple(imodes), | ||
tuple(iparams), | ||
) |
Oops, something went wrong.