summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--contrib/chromebot.json3
-rw-r--r--crocoite/cli.py1
-rw-r--r--crocoite/irc.py59
-rw-r--r--crocoite/test_irc.py19
4 files changed, 55 insertions, 27 deletions
diff --git a/contrib/chromebot.json b/contrib/chromebot.json
index 9ebd099..214b770 100644
--- a/contrib/chromebot.json
+++ b/contrib/chromebot.json
@@ -11,5 +11,6 @@
"process_limit": 1
"blacklist": {
"^https?://(.+\\.)?local(host)?/": "Not acceptable"
- }
+ },
+ "need_voice": false
}
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'<User {self.name} {self.modes}>'
+ 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)
+