diff --git a/docs/source/plugin.rst b/docs/source/plugin.rst index 09503ce387..af81c0a2be 100644 --- a/docs/source/plugin.rst +++ b/docs/source/plugin.rst @@ -8,6 +8,7 @@ Plugins: Developer Overview plugin/what plugin/anatomy plugin/bot + plugin/privileges plugin/decorators plugin/test plugin/advanced diff --git a/docs/source/plugin/anatomy.rst b/docs/source/plugin/anatomy.rst index 400f241070..09186d652f 100644 --- a/docs/source/plugin/anatomy.rst +++ b/docs/source/plugin/anatomy.rst @@ -92,6 +92,11 @@ two options: .. __: https://ircv3.net/specs/extensions/account-tag-3.2 +.. seealso:: + + Read the :doc:`privileges` chapter for more information on how to manage + privileges and access management in a plugin. + Rate limiting ------------- diff --git a/docs/source/plugin/bot.rst b/docs/source/plugin/bot.rst index bc008bc512..d700205424 100644 --- a/docs/source/plugin/bot.rst +++ b/docs/source/plugin/bot.rst @@ -281,57 +281,3 @@ You can access one user in a channel with its nick:: With the ``trigger`` object, you can also access the user object directly:: user = channel.users[trigger.nick] - -Getting user privileges in a channel ------------------------------------- - -Once you have a user's nick (either from the trigger or directly) and a -channel, you can get that user's privileges through the **channel**'s -:attr:`~sopel.tools.target.Channel.privileges` attribute:: - - user_privileges = channel.privileges['Nickname'] - user_privileges = channel.privileges[trigger.nick] - -You can check the user's privileges manually using bitwise operators. Here -for example, we check if the user is voiced (+v) or above:: - - from sopel import plugin - - if user_privileges & plugin.VOICED: - # user is voiced - elif user_privileges > plugin.VOICED: - # not voiced, but higher privileges - # like plugin.HALFOP or plugin.OP - else: - # no privilege - -Another option is to use dedicated methods from the ``channel`` object:: - - if channel.is_voiced('Nickname'): - # user is voiced - elif channel.has_privileges('Nickname', plugin.VOICED): - # not voiced, but higher privileges - # like plugin.HALFOP or plugin.OP - else: - # no privilege - -You can also iterate over the list of users and filter them by privileges:: - - # get users with the OP privilege - op_users = [ - user - for nick, user in channel.users - if channel.is_op(nick, plugin.OP) - ] - - # get users with OP privilege or above - op_or_higher_users = [ - user - for nick, user in channel.users - if channel.has_privileges(nick, plugin.OP) - ] - -.. seealso:: - - Read about the :class:`~sopel.tools.target.Channel` and - :class:`~sopel.tools.target.User` classes for more details. diff --git a/docs/source/plugin/privileges.rst b/docs/source/plugin/privileges.rst new file mode 100644 index 0000000000..39d7996659 --- /dev/null +++ b/docs/source/plugin/privileges.rst @@ -0,0 +1,182 @@ +=============== +User privileges +=============== + +IRC users can have privileges in a **channel**, given by MODE messages such as: + +.. code-block:: irc + + MODE #example +ov Nickname Nickname + +This will give both OP and Voice privileges to the user named "Nickname" in the +"#example" channel (and only in this channel). When Sopel receives a MODE +message it registers and updates its knowledge of a user's privileges in a +channel, which can be used by plugins in various way. + +Access rights +============= + +Privileged users +---------------- + +A plugin can limit who can trigger its callables using the +:func:`~sopel.plugin.require_privilege` decorator:: + + from sopel import plugin, privileges + + @plugin.require_privilege(privileges.OP) + @plugin.require_chanmsg + @plugin.command('chanopcommand') + def chanop_command(bot, trigger): + # only a channel operator can use this command + +This way, only users with OP privileges or above in a channel can use the +command ``chanopcommand`` in that channel: other users will be ignored by the +bot. It is possible to tell these users why with the ``message`` parameter:: + + @plugin.require_privilege(privileges.OP, 'You need +o privileges.') + +.. important:: + + A command that requires channel privileges will always execute if + called from a private message to the bot. You can use the + :func:`sopel.plugin.require_chanmsg` decorator to ignore the command + if it's called in PMs. + +The bot is a user too +--------------------- + +Sometimes, you may want the bot to be a privileged user in a channel to allow a +command. For that, there is the :func:`~sopel.plugin.require_bot_privilege` +decorator:: + + @plugin.require_bot_privilege(privileges.OP) + @plugin.require_chanmsg + @plugin.command('opbotcommand') + def change_topic(bot, trigger): + # only if the bot has OP privileges + +This way, this command cannot be used if the bot doesn't have the right +privileges in the channel where it is used, independent from the privileges of +the user who invokes the command. + +As with ``require_privilege``, you can provide an error message:: + + @plugin.require_bot_privilege( + privileges.OP, 'The bot needs +o privileges.') + +And you **can** use both ``require_privilege`` and ``require_bot_privilege`` on +the same plugin callable:: + + @plugin.require_privilege(privileges.VOICE) + @plugin.require_bot_privilege(privileges.OP) + @plugin.require_chanmsg + @plugin.command('special') + def special_command(bot, trigger): + # only if the user has +v and the bot has +o (or above) + +This way, you can allow a less privileged user to access a command for a more +privileged bot (this works for any combination of privileges). + +.. important:: + + A command that requires channel privileges will always execute if + called from a private message to the bot. You can use the + :func:`sopel.plugin.require_chanmsg` decorator to ignore the command + if it's called in PMs. + +Restrict to user account +------------------------ + +Sometimes, a command should be used only by users who are authenticated via +IRC services. On IRC networks that provide such information to IRC clients, +this is possible with the :func:`~sopel.plugin.require_account` decorator:: + + @plugin.require_privilege(privileges.VOICE) + @plugin.require_account + @plugin.require_chanmsg + @plugin.command('danger') + def dangerous_command(bot, trigger): + # only if the user has +v and has a registered account + +This has two consequences: + +1. this command cannot be used by users who are not authenticated +2. this command cannot be used on an IRC network that doesn't allow + authentication or doesn't expose that information + +It makes your plugin safer to use and prevents the possibility to use it on +insecure IRC networks. + +.. seealso:: + + `IRCv3 account-tracking specifications`__. + +.. __: https://ircv3.net/irc/#account-tracking + + +Getting user privileges in a channel +==================================== + +Within a :term:`plugin callable`, you can get access to a user's privileges in +a channel to check privileges manually. For example, you could adapt the level +of information your callable provides based on said privileges. + +First you need a user's nick and a channel (e.g. from the trigger parameter), +then you can get that user's privileges through the **channel**'s +:attr:`~sopel.tools.target.Channel.privileges` attribute:: + + user_privileges = channel.privileges['Nickname'] + user_privileges = channel.privileges[trigger.nick] + +You can check the user's privileges manually using bitwise operators. Here +for example, we check if the user is voiced (+v) or above:: + + from sopel import privileges + + if user_privileges & privileges.VOICE: + # user is voiced + elif user_privileges > privileges.VOICE: + # not voiced, but higher privileges + # like privileges.HALFOP or privileges.OP + else: + # no privilege + +Another option is to use dedicated methods from the ``channel`` object:: + + if channel.is_voiced('Nickname'): + # user is voiced + elif channel.has_privilege('Nickname', privileges.VOICE): + # not voiced, but higher privileges + # like privileges.HALFOP or privileges.OP + else: + # no privilege + +You can also iterate over the list of users and filter them by privileges:: + + # get users with the OP privilege + op_users = [ + user + for nick, user in channel.users + if channel.is_op(nick, privileges.OP) + ] + + # get users with OP privilege or above + op_or_higher_users = [ + user + for nick, user in channel.users + if channel.has_privileges(nick, privileges.OP) + ] + +.. seealso:: + + Read about the :class:`~sopel.tools.target.Channel` and + :class:`~sopel.tools.target.User` classes for more details. + + +sopel.privileges +================ + +.. automodule:: sopel.privileges + :members: + :member-order: bysource diff --git a/sopel/bot.py b/sopel/bot.py index 2fdf6357bd..9ad12b4388 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -19,14 +19,10 @@ from types import MappingProxyType from typing import Mapping, Optional -from sopel import irc, logger, plugins, tools -from sopel.db import SopelDB +from sopel import db, irc, logger, plugin, plugins, tools from sopel.irc import modes -import sopel.loader -from sopel.plugin import NOLIMIT from sopel.plugins import jobs as plugin_jobs, rules as plugin_rules -from sopel.tools import deprecated, Identifier -import sopel.tools.jobs +from sopel.tools import deprecated, Identifier, jobs as tools_jobs from sopel.trigger import Trigger @@ -101,7 +97,7 @@ def __init__(self, config, daemon=False): to be aware of a user, it must share at least one mutual channel. """ - self.db = SopelDB(config) + self.db = db.SopelDB(config) """The bot's database, as a :class:`sopel.db.SopelDB` instance.""" self.memory = tools.SopelMemory() @@ -139,12 +135,12 @@ def command_groups(self): ) result = {} - for plugin, commands in plugin_commands: - if plugin not in result: - result[plugin] = list(sorted(commands.keys())) + for plugin_name, commands in plugin_commands: + if plugin_name not in result: + result[plugin_name] = list(sorted(commands.keys())) else: - result[plugin].extend(commands.keys()) - result[plugin] = list(sorted(result[plugin])) + result[plugin_name].extend(commands.keys()) + result[plugin_name] = list(sorted(result[plugin_name])) return result @@ -170,7 +166,7 @@ def doc(self): ) commands = ( (command, command.get_doc(), command.get_usages()) - for plugin, commands in plugin_commands + for plugin_name, commands in plugin_commands for command in commands.values() ) @@ -315,13 +311,13 @@ def setup_plugins(self): LOGGER.info("Loading plugins...") usable_plugins = plugins.get_usable_plugins(self.settings) for name, info in usable_plugins.items(): - plugin, is_enabled = info + plugin_handler, is_enabled = info if not is_enabled: load_disabled = load_disabled + 1 continue try: - plugin.load() + plugin_handler.load() except Exception as e: load_error = load_error + 1 LOGGER.exception("Error loading %s: %s", name, e) @@ -331,9 +327,9 @@ def setup_plugins(self): "Error loading %s (plugin tried to exit)", name) else: try: - if plugin.has_setup(): - plugin.setup(self) - plugin.register(self) + if plugin_handler.has_setup(): + plugin_handler.setup(self) + plugin_handler.register(self) except Exception as e: load_error = load_error + 1 LOGGER.exception("Error in %s setup: %s", name, e) @@ -398,16 +394,16 @@ def reload_plugin(self, name): if not self.has_plugin(name): raise plugins.exceptions.PluginNotRegistered(name) - plugin = self._plugins[name] + plugin_handler = self._plugins[name] # tear down - plugin.shutdown(self) - plugin.unregister(self) + plugin_handler.shutdown(self) + plugin_handler.unregister(self) LOGGER.info("Unloaded plugin %s", name) # reload & setup - plugin.reload() - plugin.setup(self) - plugin.register(self) - meta = plugin.get_meta_description() + plugin_handler.reload() + plugin_handler.setup(self) + plugin_handler.register(self) + meta = plugin_handler.get_meta_description() LOGGER.info("Reloaded %s plugin %s from %s", meta['type'], name, meta['source']) @@ -420,17 +416,17 @@ def reload_plugins(self): """ registered = list(self._plugins.items()) # tear down all plugins - for name, plugin in registered: - plugin.shutdown(self) - plugin.unregister(self) + for name, handler in registered: + handler.shutdown(self) + handler.unregister(self) LOGGER.info("Unloaded plugin %s", name) # reload & setup all plugins - for name, plugin in registered: - plugin.reload() - plugin.setup(self) - plugin.register(self) - meta = plugin.get_meta_description() + for name, handler in registered: + handler.reload() + handler.setup(self) + handler.register(self) + meta = handler.get_meta_description() LOGGER.info("Reloaded %s plugin %s from %s", meta['type'], name, meta['source']) @@ -593,7 +589,7 @@ def register_callables(self, callables): def register_jobs(self, jobs): for func in jobs: - job = sopel.tools.jobs.Job.from_callable(self.settings, func) + job = tools_jobs.Job.from_callable(self.settings, func) self._scheduler.register(job) def unregister_jobs(self, jobs): @@ -762,7 +758,7 @@ def call(self, func, sopel, trigger): exit_code = None self.error(trigger, exception=error) - if exit_code != NOLIMIT: + if exit_code != plugin.NOLIMIT: self._times[nick][func] = current_time self._times[self.nick][func] = current_time if not trigger.is_privmsg: diff --git a/sopel/coretasks.py b/sopel/coretasks.py index e25d50d085..9572979623 100644 --- a/sopel/coretasks.py +++ b/sopel/coretasks.py @@ -31,11 +31,9 @@ import re import time -from sopel import loader, plugin -from sopel.config import ConfigurationError -from sopel.irc import isupport -from sopel.irc.utils import CapReq, MyInfo -from sopel.tools import events, Identifier, SopelMemory, target, web +from sopel import config, plugin +from sopel.irc import isupport, utils +from sopel.tools import events, Identifier, jobs, SopelMemory, target LOGGER = logging.getLogger(__name__) @@ -71,16 +69,15 @@ def setup(bot): # Manage JOIN flood protection if bot.settings.core.throttle_join: wait_interval = max(bot.settings.core.throttle_wait, 1) - - @plugin.interval(wait_interval) - @plugin.label('throttle_join') - def processing_job(bot): - _join_event_processing(bot) - - loader.clean_callable(processing_job, bot.settings) - processing_job.plugin_name = 'coretasks' - - bot.register_jobs([processing_job]) + job = jobs.Job( + [wait_interval], + plugin='coretasks', + label='throttle_join', + handler=_join_event_processing, + threaded=True, + doc=None, + ) + bot.scheduler.register(job) def shutdown(bot): @@ -400,7 +397,7 @@ def parse_reply_myinfo(bot, trigger): """Handle ``RPL_MYINFO`` events.""" # keep only # the trailing parameters (mode types) should be read from ISUPPORT - bot._myinfo = MyInfo(*trigger.args[0:3]) + bot._myinfo = utils.MyInfo(*trigger.args[0:3]) LOGGER.info( "Received RPL_MYINFO from server: %s, %s, %s", @@ -900,7 +897,7 @@ def receive_cap_list(bot, trigger): if cap == 'sasl': # TODO why is this not done with bot.cap_req? try: receive_cap_ack_sasl(bot) - except ConfigurationError as error: + except config.ConfigurationError as error: LOGGER.error(str(error)) bot.quit('Wrong SASL configuration.') @@ -943,7 +940,7 @@ def receive_cap_ls_reply(bot, trigger): ] for cap in core_caps: if cap not in bot._cap_reqs: - bot._cap_reqs[cap] = [CapReq('', 'coretasks')] + bot._cap_reqs[cap] = [utils.CapReq('', 'coretasks')] def acct_warn(bot, cap): LOGGER.info("Server does not support %s, or it conflicts with a custom " @@ -957,7 +954,7 @@ def acct_warn(bot, cap): auth_caps = ['account-notify', 'extended-join', 'account-tag'] for cap in auth_caps: if cap not in bot._cap_reqs: - bot._cap_reqs[cap] = [CapReq('', 'coretasks', acct_warn)] + bot._cap_reqs[cap] = [utils.CapReq('', 'coretasks', acct_warn)] for cap, reqs in bot._cap_reqs.items(): # At this point, we know mandatory and prohibited don't co-exist, but @@ -1010,7 +1007,7 @@ def receive_cap_ack_sasl(bot): See https://github.com/sopel-irc/sopel/issues/1780 for background """ - raise ConfigurationError( + raise config.ConfigurationError( "SASL mechanism '{}' is not advertised by this server.".format(mech)) bot.write(('AUTHENTICATE', mech)) @@ -1441,9 +1438,8 @@ def handle_url_callbacks(bot, trigger): For each URL found in the trigger, trigger the URL callback registered by the ``@url`` decorator. """ - schemes = bot.config.core.auto_url_schemes # find URLs in the trigger - for url in web.search_urls(trigger, schemes=schemes): + for url in trigger.urls: # find callbacks for said URL for function, match in bot.search_url_callbacks(url): # trigger callback defined by the `@url` decorator diff --git a/sopel/irc/backends.py b/sopel/irc/backends.py index 7b6a629712..6829007fe2 100644 --- a/sopel/irc/backends.py +++ b/sopel/irc/backends.py @@ -9,13 +9,13 @@ import asyncore import datetime import errno +import inspect import logging import os import socket import ssl import threading -from sopel import loader, plugin from sopel.tools import jobs from .abstract_backends import AbstractIRCBackend from .utils import get_cnames @@ -24,8 +24,6 @@ LOGGER = logging.getLogger(__name__) -@plugin.thread(False) -@plugin.interval(5) def _send_ping(backend): if not backend.is_connected(): return @@ -56,8 +54,6 @@ def _send_ping(backend): LOGGER.exception('Socket error on PING') -@plugin.thread(False) -@plugin.interval(10) def _check_timeout(backend): if not backend.is_connected(): return @@ -102,8 +98,8 @@ def __init__(self, bot, server_timeout=None, ping_interval=None, **kwargs): # register timeout jobs self.register_timeout_jobs([ - _send_ping, - _check_timeout, + (5, _send_ping), + (10, _check_timeout), ]) def is_connected(self): @@ -135,9 +131,13 @@ def run_forever(self): def register_timeout_jobs(self, handlers): """Register the timeout handlers for the timeout scheduler.""" - for handler in handlers: - loader.clean_callable(handler, self.bot.settings) - job = jobs.Job.from_callable(self.bot.settings, handler) + for timer, handler in handlers: + job = jobs.Job( + intervals=[timer], + handler=handler, + threaded=False, + doc=inspect.getdoc(handler), + ) self.timeout_scheduler.register(job) LOGGER.debug('Timeout Job registered: %s', str(job)) diff --git a/sopel/plugin.py b/sopel/plugin.py index e1aea22dbd..c84f6ecffa 100644 --- a/sopel/plugin.py +++ b/sopel/plugin.py @@ -12,6 +12,10 @@ import functools import re +# import and expose privileges as shortcut +from sopel.privileges import ADMIN, HALFOP, OP, OPER, OWNER, VOICE + + __all__ = [ # constants 'NOLIMIT', 'VOICE', 'HALFOP', 'OP', 'ADMIN', 'OWNER', 'OPER', @@ -61,73 +65,6 @@ .. versionadded:: 4.0 """ -VOICE = 1 -"""Privilege level for the +v channel permission - -.. versionadded:: 4.1 -""" - -HALFOP = 2 -"""Privilege level for the +h channel permission - -.. versionadded:: 4.1 - -.. important:: - - Not all IRC networks support this privilege mode. If you are writing a - plugin for public distribution, ensure your code behaves sensibly if only - ``+v`` (voice) and ``+o`` (op) modes exist. - -""" - -OP = 4 -"""Privilege level for the +o channel permission - -.. versionadded:: 4.1 -""" - -ADMIN = 8 -"""Privilege level for the +a channel permission - -.. versionadded:: 4.1 - -.. important:: - - Not all IRC networks support this privilege mode. If you are writing a - plugin for public distribution, ensure your code behaves sensibly if only - ``+v`` (voice) and ``+o`` (op) modes exist. - -""" - -OWNER = 16 -"""Privilege level for the +q channel permission - -.. versionadded:: 4.1 - -.. important:: - - Not all IRC networks support this privilege mode. If you are writing a - plugin for public distribution, ensure your code behaves sensibly if only - ``+v`` (voice) and ``+o`` (op) modes exist. - -""" - -OPER = 32 -"""Privilege level for the +y/+Y channel permissions - -Note: Except for these (non-standard) channel modes, Sopel does not monitor or -store any user's OPER status. - -.. versionadded:: 7.0.0 - -.. important:: - - Not all IRC networks support this privilege mode. If you are writing a - plugin for public distribution, ensure your code behaves sensibly if only - ``+v`` (voice) and ``+o`` (op) modes exist. - -""" - def unblockable(function): """Decorate a function to exempt it from the ignore/blocks system. diff --git a/sopel/privileges.py b/sopel/privileges.py new file mode 100644 index 0000000000..a8fe0a6a00 --- /dev/null +++ b/sopel/privileges.py @@ -0,0 +1,136 @@ +"""Constants for user privileges in channels. + +Privilege levels +================ + +Historically, there were two user privileges in channels: + +* :data:`OP`: channel operator, or chanop, set and unset by ``+o`` and ``-o`` +* :data:`VOICE`: the privilege to send messages to a channel with the + ``+m`` mode, set and unset by ``+v`` and ``-v`` + +Since then, other privileges have been adopted by IRC servers and clients: + +* :data:`HALFOP`: intermediate level between Voiced and OP, set and unset by + ``+h`` and ``-h`` +* :data:`ADMIN`: channel admin, above OP and below OWNER, set and unset by + ``+a`` and ``-a`` +* :data:`OWNER`: channel owner, above ADMIN and OP, set and unset by ``+q`` and + ``-q`` + +.. important:: + + Not all IRC networks support these added privilege modes. If you are + writing a plugin for public distribution, ensure your code behaves sensibly + if only +v (voice) and +o (op) modes exist. + +Compare privileges +================== + +This module represents privileges as powers of two, with higher values assigned +to higher-level privileges:: + + >>> from sopel.privileges import VOICE, HALFOP, OP, ADMIN, OWNER + >>> VOICE < HALFOP < OP < ADMIN < OWNER + True + +Then a user's privileges are represented as a sum of privilege levels:: + + >>> VOICE + 1 + >>> OP + 4 + >>> priv = VOICE | OP + >>> priv + 5 + +This allows to use comparators and bitwise operators to compare privileges:: + + >>> priv >= OP + True + >>> bool(priv & HALFOP) + False + +In that case, ``priv`` contains both VOICE and OP privileges, but not HALFOP. +""" +from __future__ import generator_stop + + +VOICE = 1 +"""Privilege level for the +v channel permission + +.. versionadded:: 4.1 +.. versionchanged:: 8.0 + Moved into :mod:`sopel.privileges`. +""" + +HALFOP = 2 +"""Privilege level for the +h channel permission + +.. versionadded:: 4.1 +.. versionchanged:: 8.0 + Moved into :mod:`sopel.privileges`. + +.. important:: + + Not all IRC networks support this privilege mode. If you are writing a + plugin for public distribution, ensure your code behaves sensibly if only + ``+v`` (voice) and ``+o`` (op) modes exist. + +""" + +OP = 4 +"""Privilege level for the +o channel permission + +.. versionadded:: 4.1 +.. versionchanged:: 8.0 + Moved into :mod:`sopel.privileges`. +""" + +ADMIN = 8 +"""Privilege level for the +a channel permission + +.. versionadded:: 4.1 +.. versionchanged:: 8.0 + Moved into :mod:`sopel.privileges`. + +.. important:: + + Not all IRC networks support this privilege mode. If you are writing a + plugin for public distribution, ensure your code behaves sensibly if only + ``+v`` (voice) and ``+o`` (op) modes exist. + +""" + +OWNER = 16 +"""Privilege level for the +q channel permission + +.. versionadded:: 4.1 +.. versionchanged:: 8.0 + Moved into :mod:`sopel.privileges`. + +.. important:: + + Not all IRC networks support this privilege mode. If you are writing a + plugin for public distribution, ensure your code behaves sensibly if only + ``+v`` (voice) and ``+o`` (op) modes exist. + +""" + +OPER = 32 +"""Privilege level for the +y/+Y channel permissions + +Note: Except for these (non-standard) channel modes, Sopel does not monitor or +store any user's OPER status. + +.. versionadded:: 7.0 +.. versionchanged:: 8.0 + Moved into :mod:`sopel.privileges`. + +.. important:: + + Not all IRC networks support this privilege mode. If you are writing a + plugin for public distribution, ensure your code behaves sensibly if only + ``+v`` (voice) and ``+o`` (op) modes exist. + +""" diff --git a/sopel/tools/target.py b/sopel/tools/target.py index d12bd08b08..37a0321c41 100644 --- a/sopel/tools/target.py +++ b/sopel/tools/target.py @@ -2,7 +2,7 @@ import functools -from sopel import plugin +from sopel import privileges from sopel.tools import Identifier @@ -193,7 +193,7 @@ def is_oper(self, nick): sensibly if only ``+v`` (voice) and ``+o`` (op) modes exist. """ - return self.privileges.get(Identifier(nick), 0) & plugin.OPER + return self.privileges.get(Identifier(nick), 0) & privileges.OPER def is_owner(self, nick): """Tell if a user has the OWNER privilege level. @@ -225,7 +225,7 @@ def is_owner(self, nick): sensibly if only ``+v`` (voice) and ``+o`` (op) modes exist. """ - return self.privileges.get(Identifier(nick), 0) & plugin.OWNER + return self.privileges.get(Identifier(nick), 0) & privileges.OWNER def is_admin(self, nick): """Tell if a user has the ADMIN privilege level. @@ -257,7 +257,7 @@ def is_admin(self, nick): sensibly if only ``+v`` (voice) and ``+o`` (op) modes exist. """ - return self.privileges.get(Identifier(nick), 0) & plugin.ADMIN + return self.privileges.get(Identifier(nick), 0) & privileges.ADMIN def is_op(self, nick): """Tell if a user has the OP privilege level. @@ -283,7 +283,7 @@ def is_op(self, nick): True """ - return self.privileges.get(Identifier(nick), 0) & plugin.OP + return self.privileges.get(Identifier(nick), 0) & privileges.OP def is_halfop(self, nick): """Tell if a user has the HALFOP privilege level. @@ -315,7 +315,7 @@ def is_halfop(self, nick): sensibly if only ``+v`` (voice) and ``+o`` (op) modes exist. """ - return self.privileges.get(Identifier(nick), 0) & plugin.HALFOP + return self.privileges.get(Identifier(nick), 0) & privileges.HALFOP def is_voiced(self, nick): """Tell if a user has the VOICE privilege level. @@ -342,7 +342,7 @@ def is_voiced(self, nick): True """ - return self.privileges.get(Identifier(nick), 0) & plugin.VOICE + return self.privileges.get(Identifier(nick), 0) & privileges.VOICE def rename_user(self, old, new): """Rename a user.