From 341b3141048a72b59593d17713bf492d5e115ac2 Mon Sep 17 00:00:00 2001 From: jesopo Date: Sat, 25 Jan 2020 13:58:13 +0000 Subject: [PATCH] change command specs to be compiled at runtime by a decorator --- modules/channel_op.py | 34 +++++++++++----------- modules/echo.py | 6 ++-- src/core_modules/command_spec.py | 48 +++++++++++++------------------- src/utils/__init__.py | 2 +- src/utils/decorators.py | 15 ++++++++-- src/utils/parse.py | 27 ++++++++++++++++++ 6 files changed, 79 insertions(+), 53 deletions(-) diff --git a/modules/channel_op.py b/modules/channel_op.py index f8a49fae..e8fe8095 100644 --- a/modules/channel_op.py +++ b/modules/channel_op.py @@ -49,7 +49,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "kick") @utils.kwarg("usage", " [reason]") - @utils.kwarg("spec", "!r~channel !cuser ?...") + @utils.spec("!r~channel !cuser ?...") def kick(self, event): self._kick(event["server"], event["target"], event["spec"][0], event["spec"][1]) @@ -99,7 +99,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "ban") @utils.kwarg("usage", "[+time] ") - @utils.kwarg("spec", "!r~channel ?time !user|text") + @utils.spec("!r~channel ?time !user|text") def ban(self, event): self._ban(event["server"], event["spec"][0], event["spec"][2], True, event["spec"][1], True) @@ -108,7 +108,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "ban") @utils.kwarg("usage", "") - @utils.kwarg("spec", "!r~channel !user|word") + @utils.spec("!r~channel !user|word") def unban(self, event): self._ban(event["server"], event["spec"][0], event["spec"][1], True, None, False) @@ -118,7 +118,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "kickban") @utils.kwarg("usage", "[+time] [reason]") - @utils.kwarg("spec", "!r~channel ?time !cuser| ?...") + @utils.spec("!r~channel ?time !cuser| ?...") def kickban(self, event): self._ban(event["server"], event["spec"][0], event["spec"][2], False, event["spec"][1], True) @@ -130,7 +130,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "op") @utils.kwarg("usage", "[nickname]") - @utils.kwarg("spec", "!r~channel !ruser") + @utils.spec("!r~channel !ruser") def op(self, event): self._op(True, event["spec"]) @@ -139,7 +139,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "op") @utils.kwarg("usage", "[nickname]") - @utils.kwarg("spec", "!r~channel !ruser") + @utils.spec("!r~channel !ruser") def deop(self, event): self._op(False, event["spec"]) @@ -151,7 +151,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "voice") @utils.kwarg("usage", "[nickname]") - @utils.kwarg("spec", "!r~channel !ruser") + @utils.spec("!r~channel !ruser") def voice(self, event): add = event["command"] == "voice" event["spec"][0].send_mode("+v" if add else "-v", [event["spec"][1]]) @@ -161,7 +161,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_access", "topic") @utils.kwarg("remove_empty", False) @utils.kwarg("usage", "") - @utils.kwarg("spec", "!r~channel !...") + @utils.spec("!r~channel !...") def topic(self, event): event["spec"][0].send_topic(event["spec"][1]) @@ -170,7 +170,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_access", "topic") @utils.kwarg("remove_empty", False) @utils.kwarg("usage", "") - @utils.kwarg("spec", "!r~channel !...") + @utils.spec("!r~channel !...") def tappend(self, event): event["spec"][0].send_topic(event["spec"][0].topic + event["spec"][1]) @@ -193,7 +193,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_access", "quiet") @utils.kwarg("help", "Quiet a given user") @utils.kwarg("usage", "[+time] ") - @utils.kwarg("spec", "!r~channel ?time !user|word") + @utils.spec("!r~channel ?time !user|word") def quiet(self, event): self._quiet(event["server"], True, event["spec"]) @@ -203,7 +203,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_access", "unquiet") @utils.kwarg("help", "Unquiet a given user") @utils.kwarg("usage", "") - @utils.kwarg("spec", "!r~channel !user|word") + @utils.spec("!r~channel !user|word") def unquiet(self, event): self._quiet(event["server"], False, event["spec"]) @@ -239,7 +239,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_access", "invite") @utils.kwarg("help", "Invite a given user") @utils.kwarg("usage", "") - @utils.kwarg("spec", "!r~channel !word") + @utils.spec("!r~channel !word") def invite(self, event): user_nickname = event["args_split"][0] @@ -264,7 +264,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "flags") @utils.kwarg("usage", " [flags]") - @utils.kwarg("spec", "!r~channel !ouser ?...") + @utils.spec("!r~channel !ouser ?...") def flags(self, event): target = event["spec"][1] current_flags = event["spec"][0].get_user_setting(target.get_id(), @@ -355,7 +355,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_access", "cmute") @utils.kwarg("help", "Mute the current channel") @utils.kwarg("usage", "[+time]") - @utils.kwarg("spec", "!r~channel ?time") + @utils.spec("!r~channel ?time") def cmute(self, event): event["spec"][0].send_mode("+m") @@ -373,7 +373,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("require_mode", "o") @utils.kwarg("require_access", "cmute") @utils.kwarg("help", "Mute the current channel") - @utils.kwarg("spec", "!r~channel") + @utils.spec("!r~channel") def cunmute(self, event): self._cunmute(event["spec"][0]) @@ -421,7 +421,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("help", "Clear a given channel list mode (e.g. +b)") @utils.kwarg("usage", " [mask]") @utils.kwarg("usage", "+ [mask]") - @utils.kwarg("spec", "!r~channel !word ?word") + @utils.spec("!r~channel !word ?word") def clear(self, event): mode, mode_list = self._list_query_event( event["server"], event["spec"][0], event["spec"][1], @@ -437,7 +437,7 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("help", "Search a given channel list mode (e.g. +b)") @utils.kwarg("usage", " [mask]") @utils.kwarg("usage", "+ [mask]") - @utils.kwarg("spec", "!r~channel !word ?word") + @utils.spec("!r~channel !word ?word") def lsearch(self, event): mode, mode_list = self._list_query_event( event["server"], event["spec"][0], event["spec"][1], diff --git a/modules/echo.py b/modules/echo.py index d1d9d2cb..8276bd02 100644 --- a/modules/echo.py +++ b/modules/echo.py @@ -6,14 +6,14 @@ class Module(ModuleManager.BaseModule): @utils.hook("received.command.echo") @utils.kwarg("remove_empty", False) @utils.kwarg("help", "Echo a string back") - @utils.kwarg("spec", "!...") + @utils.spec("!...") def echo(self, event): event["stdout"].write(event["spec"][0]) @utils.hook("received.command.action") @utils.kwarg("remove_empty", False) @utils.kwarg("help", "Make the bot send a /me") - @utils.kwarg("spec", "!...") + @utils.spec("!...") def action(self, event): event["target"].send_message("\x01ACTION %s\x01" % event["spec"][0]) @@ -21,6 +21,6 @@ class Module(ModuleManager.BaseModule): @utils.kwarg("permission", "say") @utils.kwarg("remove_empty", False) @utils.kwarg("help", "Send a message to a target") - @utils.kwarg("spec", "!word !...") + @utils.spec("!word !...") def msg(self, event): event["server"].send_message(event["spec"][0], event["spec"][1]) diff --git a/src/core_modules/command_spec.py b/src/core_modules/command_spec.py index 96a08e42..98b3c1db 100644 --- a/src/core_modules/command_spec.py +++ b/src/core_modules/command_spec.py @@ -38,12 +38,12 @@ class Module(ModuleManager.BaseModule): n = 0 error = None - if spec_type == "time" and args: + if spec_type.name == "time" and args: time, _ = utils.parse.timed_args(args) chunk = time n = 1 error = "Invalid timeframe" - elif spec_type == "rchannel": + elif spec_type.name == "rchannel": if channel: chunk = channel elif args: @@ -53,53 +53,53 @@ class Module(ModuleManager.BaseModule): error = "No such channel" else: error = "No channel provided" - elif spec_type == "channel" and args: + elif spec_type.name == "channel" and args: if args[0] in server.channels: chunk = server.channels.get(args[0]) n = 1 error = "No such channel" - elif spec_type == "cuser" and args: + elif spec_type.name == "cuser" and args: tuser = server.get_user(args[0], create=False) if tuser and channel.has_user(tuser): chunk = tuser n = 1 error = "That user is not in this channel" - elif spec_type == "ruser": + elif spec_type.name == "ruser": if args: chunk = server.get_user(args[0], create=False) n = 1 else: chunk = user error = "No such user" - elif spec_type == "user": + elif spec_type.name == "user": if args: chunk = server.get_user(args[0], create=False) n = 1 error = "No such user" else: error = "No user provided" - elif spec_type == "ouser" and args: + elif spec_type.name == "ouser" and args: if server.has_user_id(args[0]): chunk = server.get_user(args[0]) n = 1 error = "Unknown nickname" - elif spec_type == "word": + elif spec_type.name == "word": if args: chunk = args[0] n = 1 - elif spec_type == "...": + elif spec_type.name == "...": if args: chunk = " ".join(args) n = max(1, len(args)) - options.append([chunk, n, error]) + options.append([spec_type, chunk, n, error]) return options @utils.hook("preprocess.command") @utils.kwarg("priority", EventManager.PRIORITY_HIGH) def preprocess(self, event): - spec = event["hook"].get_kwarg("spec", None) - if not spec == None: + spec_types = event["hook"].get_kwarg("spec", None) + if not spec_types == None: server = event["server"] channel = event["target"] if event["is_channel"] else None user = event["user"] @@ -108,30 +108,20 @@ class Module(ModuleManager.BaseModule): out = [] kwargs = {"channel": channel} - for word in spec.split(): - optional = word[0] == "?" - word = word[1:] - - raw_spec_types = word.split("|") - spec_types = [t.replace("~", "", 1) for t in raw_spec_types] - + for item in spec_types: options = self._spec_chunk(server, kwargs["channel"], user, - spec_types, args) + item.types, args) found = None first_error = None - for i, (chunk, n, error) in enumerate(options): - spec_type = spec_types[i] - raw_spec_type = raw_spec_types[i] - - + for spec_type, chunk, n, error in options: if not chunk == None: - if "~" in raw_spec_type: - kwargs[raw_spec_type.split("~", 1)[1]] = chunk + if spec_type.exported: + kwargs[spec_type.exported] = chunk found = True args = args[n:] - if len(spec_types) > 1: + if len(item.types) > 1: chunk = [spec_type, chunk] found = chunk break @@ -143,7 +133,7 @@ class Module(ModuleManager.BaseModule): out.append(found) - if not optional and not found: + if not item.optional and not found: error = first_error or "Invalid arguments" return utils.consts.PERMISSION_HARD_FAIL, error diff --git a/src/utils/__init__.py b/src/utils/__init__.py index aad6f7a8..303c70c5 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -2,7 +2,7 @@ import contextlib, enum, ipaddress, multiprocessing, queue, signal, threading import typing from . import cli, consts, datetime, decorators, irc, http, parse, security -from .decorators import export, hook, kwarg +from .decorators import export, hook, kwarg, spec from .settings import (BoolSetting, FunctionSetting, IntRangeSetting, IntSetting, OptionsSetting, sensitive_format, SensitiveSetting, Setting) from .errors import (EventError, EventNotEnoughArgsError, EventResultsError, diff --git a/src/utils/decorators.py b/src/utils/decorators.py index dc4adf3a..ca47ea6e 100644 --- a/src/utils/decorators.py +++ b/src/utils/decorators.py @@ -1,4 +1,5 @@ import typing +from .parse import argument_spec BITBOT_MAGIC = "__bitbot" @@ -42,10 +43,18 @@ def export(setting: str, value: typing.Any): magic.add_export(setting, value) return module return _export_func + +def _kwarg(key: str, value: typing.Any, func: typing.Any): + magic = get_magic(func) + magic.add_kwarg(key, value) + return func + def kwarg(key: str, value: typing.Any): def _kwarg_func(func): - magic = get_magic(func) - magic.add_kwarg(key, value) - return func + return _kwarg(key, value, func) return _kwarg_func +def spec(spec: str): + def _spec_func(func): + return _kwarg("spec", argument_spec(spec), func) + return _spec_func diff --git a/src/utils/parse.py b/src/utils/parse.py index b45ca6fe..258b97dc 100644 --- a/src/utils/parse.py +++ b/src/utils/parse.py @@ -155,3 +155,30 @@ def format_token_replace(s: str, vars: typing.Dict[str, str], for i, token in tokens: s = s[:i] + vars[token.replace(sigil, "", 1)] + s[i+len(token):] return s + +class ArgumentSpecType(object): + def __init__(self, name: str, exported: str): + self.name = name + self.exported = exported + +class ArgumentSpec(object): + def __init__(self, optional: bool, types: typing.List[ArgumentSpecType]): + self.optional = optional + self.types = types + +def argument_spec(spec: str) -> typing.List[ArgumentSpec]: + out: typing.List[ArgumentSpec] = [] + for type_names_str in spec.split(" "): + optional = type_names_str[0] == "?" + type_names_str = type_names_str[1:] + + spec_types: typing.List[ArgumentSpecType] = [] + for type_name in type_names_str.split("|"): + exported_name = "" + if "~" in type_name: + exported_name = type_name + type_name = type_name.replace("~", "", 1) + + spec_types.append(ArgumentSpecType(type_name, exported_name)) + out.append(ArgumentSpec(optional, spec_types)) + return out