From 36876a5d608ccfc3d594fa6906f04b4feca08142 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Fri, 8 Mar 2019 10:05:12 +0100 Subject: irc: Add config option need_voice Do not hardcode required priviledge to use bot, make it configureable. --- crocoite/cli.py | 1 + crocoite/irc.py | 59 ++++++++++++++++++++++++++++++---------------------- crocoite/test_irc.py | 19 ++++++++++++++++- 3 files changed, 53 insertions(+), 26 deletions(-) (limited to 'crocoite') diff --git a/crocoite/cli.py b/crocoite/cli.py index 4ab2334..1f22c45 100644 --- a/crocoite/cli.py +++ b/crocoite/cli.py @@ -164,6 +164,7 @@ def irc (): processLimit=config['process_limit'], logger=logger, blacklist=blacklist, + needVoice=config['need_voice'], loop=loop) stop = lambda signum: bot.cancel () loop.add_signal_handler (signal.SIGINT, stop, signal.SIGINT) diff --git a/crocoite/irc.py b/crocoite/irc.py index 0b01413..973d7d1 100644 --- a/crocoite/irc.py +++ b/crocoite/irc.py @@ -25,7 +25,7 @@ IRC bot “chromebot” import asyncio, argparse, uuid, json, tempfile from datetime import datetime from urllib.parse import urlsplit -from enum import IntEnum, Enum +from enum import IntEnum, unique from collections import defaultdict from abc import abstractmethod from functools import wraps @@ -112,14 +112,25 @@ class Job: f"{stats.get ('failed', 0)} failed, " f"{prettyBytes (stats.get ('bytesRcv', 0))} received.") -class NickMode(Enum): - operator = '@' - voice = '+' +@unique +class NickMode(IntEnum): + # the actual numbers don’t matter, but their order must be strictly + # increasing (with priviledge level) + operator = 100 + voice = 10 @classmethod def fromMode (cls, mode): return {'v': cls.voice, 'o': cls.operator}[mode] + @classmethod + def fromNickPrefix (cls, mode): + return {'@': cls.operator, '+': cls.voice}[mode] + + @property + def human (self): + return {self.operator: 'operator', self.voice: 'voice'}[self] + class User: """ IRC user """ __slots__ = ('name', 'modes') @@ -137,13 +148,19 @@ class User: def __repr__ (self): return f'' + def hasPriv (self, p): + if p is None: + return True + else: + return self.modes and max (self.modes) >= p + @classmethod def fromName (cls, name): """ Get mode and name from NAMES command """ try: - modes = {NickMode(name[0])} + modes = {NickMode.fromNickPrefix (name[0])} name = name[1:] - except ValueError: + except KeyError: modes = set () return cls (name, modes) @@ -330,8 +347,11 @@ class ArgparseBot (bottom.Client): reply (f'Sorry, I don’t understand {command}') return + minPriv = getattr (args, 'minPriv', None) if self._quit.armed and not getattr (args, 'allowOnShutdown', False): reply ('Sorry, I’m shutting down and cannot accept your request right now.') + elif not user.hasPriv (minPriv): + reply (f'Sorry, you need the privilege {minPriv.human} to use this command.') else: with self._quit: await args.func (user=user, args=args, reply=reply) @@ -348,19 +368,6 @@ class ArgparseBot (bottom.Client): finally: break -def voice (func): - """ Calling user must have voice or ops """ - @wraps (func) - async def inner (self, *args, **kwargs): - user = kwargs.get ('user') - reply = kwargs.get ('reply') - if not user.modes.intersection ({NickMode.operator, NickMode.voice}): - reply ('Sorry, you must have voice to use this command.') - else: - ret = await func (self, *args, **kwargs) - return ret - return inner - def jobExists (func): """ Chromebot job exists """ @wraps (func) @@ -377,11 +384,13 @@ def jobExists (func): return inner class Chromebot (ArgparseBot): - __slots__ = ('jobs', 'tempdir', 'destdir', 'processLimit', 'blacklist') + __slots__ = ('jobs', 'tempdir', 'destdir', 'processLimit', 'blacklist', 'needVoice') def __init__ (self, host, port, ssl, nick, logger, channels=None, tempdir=None, destdir='.', processLimit=1, - blacklist={}, loop=None): + blacklist={}, needVoice=False, loop=None): + self.needVoice = needVoice + super().__init__ (host=host, port=port, ssl=ssl, nick=nick, logger=logger, channels=channels, loop=loop) @@ -402,7 +411,8 @@ class Chromebot (ArgparseBot): archiveparser.add_argument('--concurrency', '-j', default=1, type=int, help='Parallel workers for this job', choices=range (1, 5)) archiveparser.add_argument('--recursive', '-r', help='Enable recursion', choices=['0', '1', 'prefix'], default='0') archiveparser.add_argument('url', help='Website URL', type=isValidUrl, metavar='URL') - archiveparser.set_defaults (func=self.handleArchive) + archiveparser.set_defaults (func=self.handleArchive, + minPriv=NickMode.voice if self.needVoice else None) statusparser = subparsers.add_parser ('s', help='Get job status', add_help=False) statusparser.add_argument('id', help='Job id', metavar='UUID') @@ -410,7 +420,8 @@ class Chromebot (ArgparseBot): abortparser = subparsers.add_parser ('r', help='Revoke/abort job', add_help=False) abortparser.add_argument('id', help='Job id', metavar='UUID') - abortparser.set_defaults (func=self.handleAbort, allowOnShutdown=True) + abortparser.set_defaults (func=self.handleAbort, allowOnShutdown=True, + minPriv=NickMode.voice if self.needVoice else None) return parser @@ -420,7 +431,6 @@ class Chromebot (ArgparseBot): return v return False - @voice async def handleArchive (self, user, args, reply): """ Handle the archive command """ @@ -496,7 +506,6 @@ class Chromebot (ArgparseBot): rstats = job.rstats reply (job.formatStatus ()) - @voice @jobExists async def handleAbort (self, user, args, reply, job): """ Handle abort command """ diff --git a/crocoite/test_irc.py b/crocoite/test_irc.py index 4d80a6d..9344de4 100644 --- a/crocoite/test_irc.py +++ b/crocoite/test_irc.py @@ -19,7 +19,7 @@ # THE SOFTWARE. import pytest -from .irc import ArgparseBot, RefCountEvent +from .irc import ArgparseBot, RefCountEvent, User, NickMode def test_mode_parse (): assert ArgparseBot.parseMode ('+a') == [('+', 'a')] @@ -51,3 +51,20 @@ def test_refcountevent_arm_with (event): event.arm () assert not event.event.is_set () assert event.event.is_set () + +def test_nick_mode (): + a = User.fromName ('a') + a2 = User.fromName ('a') + a3 = User.fromName ('+a') + b = User.fromName ('+b') + c = User.fromName ('@c') + + # equality is based on name only, not mode + assert a == a2 + assert a == a3 + assert a != b + + assert a.hasPriv (None) and not a.hasPriv (NickMode.voice) and not a.hasPriv (NickMode.operator) + assert b.hasPriv (None) and b.hasPriv (NickMode.voice) and not b.hasPriv (NickMode.operator) + assert c.hasPriv (None) and c.hasPriv (NickMode.voice) and c.hasPriv (NickMode.operator) + -- cgit v1.2.3