From 5730d6fe591b63b8d50fbebf7c151504003921bd Mon Sep 17 00:00:00 2001 From: jesopo Date: Fri, 22 Nov 2019 16:23:30 +0000 Subject: [PATCH] refactor commands (mostly stdout/stderr) and split typing/reply out closes #208 --- modules/commands/__init__.py | 132 +++++++++++++++++------------------ modules/commands/outs.py | 101 +++++---------------------- modules/ircv3_msgid.py | 7 ++ modules/ircv3_typing.py | 27 +++++++ src/ModuleManager.py | 10 +-- 5 files changed, 119 insertions(+), 158 deletions(-) create mode 100644 modules/ircv3_typing.py diff --git a/modules/commands/__init__.py b/modules/commands/__init__.py index 433e1097..5b0a52e8 100644 --- a/modules/commands/__init__.py +++ b/modules/commands/__init__.py @@ -8,9 +8,10 @@ from . import outs COMMAND_METHOD = "command-method" COMMAND_METHODS = ["PRIVMSG", "NOTICE"] -MESSAGE_TAGS_CAP = utils.irc.Capability("message-tags", - "draft/message-tags-0.2") -MSGID_TAG = utils.irc.MessageTag("msgid", "draft/msgid") +STR_MORE = " (more...)" +STR_MORE_LEN = len(STR_MORE.encode("utf8")) +STR_CONTINUED = "(...continued)" +WORD_BOUNDARIES = [" "] NON_ALPHANUMERIC = [char for char in string.printable if not char.isalnum()] @@ -48,8 +49,6 @@ class Module(ModuleManager.BaseModule): target = event["user"] else: target = event["channel"] - target.last_stdout = None - target.last_stderr = None def has_command(self, command): return command.lower() in self.events.on("received").on( @@ -62,10 +61,10 @@ class Module(ModuleManager.BaseModule): if s and s[-1] in [":", ","]: return server.is_own_nickname(s[:-1]) - def _command_method(self, target, server): + def _command_method(self, server, target): return target.get_setting(COMMAND_METHOD, server.get_setting(COMMAND_METHOD, - self.bot.get_setting(COMMAND_METHOD, "PRIVMSG"))) + self.bot.get_setting(COMMAND_METHOD, "PRIVMSG"))).upper() def _find_command_hook(self, server, target, is_channel, command, args): if not self.has_command(command): @@ -159,31 +158,13 @@ class Module(ModuleManager.BaseModule): if not is_success: raise utils.EventError("%s: %s" % (user.nickname, message)) - def _tagmsg(self, target, tags): - return IRCLine.ParsedLine("TAGMSG", [target], tags=tags) - def command(self, server, target, target_str, is_channel, user, command, - args_split, tags, hook, **kwargs): - message_tags = server.has_capability(MESSAGE_TAGS_CAP) - expect_output = hook.get_kwarg("expect_output", True) + args_split, line, hook, **kwargs): + module_name = (self._get_prefix(hook) or + self.bot.modules.from_context(hook.context).title) - module_name = self._get_prefix(hook) or "" - if not module_name and hasattr(hook.function, "__self__"): - module_name = hook.function.__self__._name - - send_tags = {} - if message_tags: - msgid = MSGID_TAG.get_value(tags) - if msgid: - send_tags["+draft/reply"] = msgid - - if expect_output: - line = self._tagmsg(target_str, {"+draft/typing": "active"}) - server.send(line, immediate=True) - - stdout = outs.StdOut(server, module_name, target, target_str, send_tags) - stderr = outs.StdErr(server, module_name, target, target_str, send_tags) - command_method = self._command_method(target, server) + stdout = outs.StdOut(module_name) + stderr = outs.StdOut(module_name) ret = False has_out = False @@ -192,9 +173,9 @@ class Module(ModuleManager.BaseModule): args_split = list(filter(None, args_split)) event_kwargs = {"hook": hook, "user": user, "server": server, - "target": target, "is_channel": is_channel, "tags": tags, - "args_split": args_split, "command": command, - "args": " ".join(args_split), "stdout": stdout, + "target": target, "target_str": target_str, + "is_channel": is_channel, "line": line, "args_split": args_split, + "command": command, "args": " ".join(args_split), "stdout": stdout, "stderr": stderr} event_kwargs.update(kwargs) @@ -203,37 +184,59 @@ class Module(ModuleManager.BaseModule): event_kwargs["check_assert"] = check_assert check_success, check_message = self._check("preprocess", event_kwargs) - if not check_success: + if check_success: + new_event = self.events.on(hook.event_name).make_event(**event_kwargs) + self.log.trace("calling command '%s': %s", [command, new_event.kwargs]) + + try: + hook.call(new_event) + except utils.EventError as e: + stderr.write(str(e)) + else: if check_message: - stderr.write("%s: %s" % (user.nickname, check_message) - ).send(command_method) - return True + stderr.write("%s: %s" % (user.nickname, check_message)) - new_event = self.events.on(hook.event_name).make_event(**event_kwargs) + self._check("postprocess", event_kwargs) + # postprocess - send stdout/stderr and typing tag - self.log.trace("calling command '%s': %s", [command, new_event.kwargs]) + return new_event.eaten - try: - hook.call(new_event) - except utils.EventError as e: - stderr.write(str(e)).send(command_method) - return True + @utils.hook("postprocess.command") + @utils.kwarg("priority", EventManager.PRIORITY_LOW) + def postprocess(self, event): + color = None + obj = None + if event["stdout"].has_text(): + color = utils.consts.GREEN + obj = event["stdout"] + elif event["stderr"].has_text(): + color = utils.consts.RED + obj = event["stderr"] + else: + return - if not hook.get_kwarg("skip_out", False): - has_out = stdout.has_text() or stderr.has_text() - if has_out: - command_method = self._command_method(target, server) - stdout.send(command_method) - stderr.send(command_method) - target.last_stdout = stdout - target.last_stderr = stderr - ret = new_event.eaten + line_str = "[%s] %s" % (utils.irc.color(obj.prefix, color), obj.pop()) + method = self._command_method(event["server"], event["target"]) - if expect_output and message_tags and not has_out: - line = self._tagmsg(target_str, {"+draft/typing": "done"}) - server.send(line, immediate=True) + if not method in ["PRIVMSG", "NOTICE"]: + raise ValueError("Unknown command-method '%s'" % method) - return ret + line = IRCLine.ParsedLine(method, [event["target_str"], line_str], + tags=obj.tags) + valid, trunc = line.truncate(event["server"].hostmask(), + margin=STR_MORE_LEN) + + if trunc: + if not trunc[0] in WORD_BOUNDARIES: + for boundary in WORD_BOUNDARIES: + left, *right = valid.rsplit(boundary, 1) + if right: + valid = left + trunc = right[0]+trunc + obj.insert("%s %s" % (STR_CONTINUED, trunc)) + valid = valid+STR_MORE + line = IRCLine.parse_line(valid) + event["server"].send(line) @utils.hook("preprocess.command") def _check_min_args(self, event): @@ -293,7 +296,7 @@ class Module(ModuleManager.BaseModule): if hook: self.command(event["server"], event["channel"], event["target_str"], True, event["user"], command, - args_split, event["tags"], hook, + args_split, event["line"], hook, command_prefix=command_prefix) else: self.events.on("unknown.command").call(server=event["server"], @@ -313,7 +316,7 @@ class Module(ModuleManager.BaseModule): command = hook.get_kwarg("command", "") res = self.command(event["server"], event["channel"], event["target_str"], True, event["user"], command, - "", event["tags"], hook, match=match, + "", event["line"], hook, match=match, message=event["message"], command_prefix="", action=event["action"]) @@ -344,7 +347,7 @@ class Module(ModuleManager.BaseModule): if hook: self.command(event["server"], event["user"], event["user"].nickname, False, event["user"], command, - args_split, event["tags"], hook, command_prefix="") + args_split, event["line"], hook, command_prefix="") else: self.events.on("unknown.command").call(server=event["server"], target=event["user"], user=event["user"], command=command, @@ -364,15 +367,6 @@ class Module(ModuleManager.BaseModule): def _get_alias_of(self, hook): return hook.get_kwarg("alias_of", None) - @utils.hook("received.command.more", skip_out=True) - def more(self, event): - """ - :help: Show more output from the last command - """ - if event["target"].last_stdout and event["target"].last_stdout.has_text(): - event["target"].last_stdout.send( - self._command_method(event["target"], event["server"])) - @utils.hook("send.stdout") def send_stdout(self, event): target = event["target"] diff --git a/modules/commands/outs.py b/modules/commands/outs.py index 41528da3..fb3c29ef 100644 --- a/modules/commands/outs.py +++ b/modules/commands/outs.py @@ -1,98 +1,29 @@ import re from src import IRCLine, utils -STR_MORE = " (more...)" -STR_MORE_LEN = len(STR_MORE.encode("utf8")) -STR_CONTINUED = "(...continued) " -WORD_BOUNDARY = ' ' - -def _message_factory(command): - if not command in ["PRIVMSG", "NOTICE"]: - raise ValueError("Unknown command method '%s'" % method) - - def _(target, message, tags): - return IRCLine.ParsedLine(command, [target, message], tags=tags) - return _ - -class Out(object): - def __init__(self, server, module_name, target, target_str, tags): - self.server = server - self._prefix = self._default_prefix(module_name) - self._hide_prefix = False - self.target = target - self._target_str = target_str - self._text = "" - self.written = False - self._tags = tags +class StdOut(object): + def __init__(self, prefix): + self.prefix = prefix + self._lines = [] + self.tags = {} self._assured = False def assure(self): self._assured = True def write(self, text): - self._text += text - self.written = True - return self - def writeline(self, line): - self._text += "%s\n" % line + self.write_lines( + text.replace("\r", "").replace("\n\n", "\n").split("\n")) + def write_lines(self, lines): + self._lines += list(filter(None, lines)) - def send(self, method): - if self.has_text(): - prefix = "" - if not self._hide_prefix: - prefix = utils.consts.RESET + "[%s] " % self._prefix - - text = self._text[:].replace("\r", "") - while "\n\n" in text: - text = text.replace("\n\n", "\n") - - full_text = "%s%s" % (prefix, text) - message_factory = _message_factory(method) - - line = message_factory(self._target_str, full_text, tags=self._tags) - if self._assured: - line.assure() - - valid, truncated = line.truncate(self.server.hostmask(), - margin=STR_MORE_LEN) - - if truncated: - valid, truncated = self._adjust_to_word_boundaries(valid, truncated) - - line = IRCLine.parse_line(valid+STR_MORE) - self._text = "%s%s" % (STR_CONTINUED, truncated) - else: - self._text = "" - - sent_line = self.server.send(line) - - def _adjust_to_word_boundaries(self, left, right): - if right[0] == WORD_BOUNDARY: - return left, right - - parts = left.rsplit(WORD_BOUNDARY, 1) - - if len(parts) != 2: - return left, right - - return parts[0], parts[1] + right - - def _default_prefix(self, s: str): - return s - def set_prefix(self, prefix): - self._prefix = self._default_prefix(prefix) - def append_prefix(self, s: str): - self._prefix = "%s%s" % (self._prefix, s) - def hide_prefix(self): - self._hide_prefix = True + def get_all(self): + return self._lines.copy() + def pop(self): + return self._lines.pop(0) + def insert(self, text): + self._lines.insert(0, text) def has_text(self): - return bool(self._text) - -class StdOut(Out): - def _default_prefix(self, s: str): - return utils.irc.color(s, utils.consts.GREEN) -class StdErr(Out): - def _default_prefix(self, s: str): - return utils.irc.color(s, utils.consts.RED) + return bool(self._lines) diff --git a/modules/ircv3_msgid.py b/modules/ircv3_msgid.py index d5690286..cbb207dc 100644 --- a/modules/ircv3_msgid.py +++ b/modules/ircv3_msgid.py @@ -22,3 +22,10 @@ class Module(ModuleManager.BaseModule): def ctcp(self, event): if event["is_channel"]: self._on_channel(event["target"], event["tags"]) + + @utils.hook("postprocess.command") + def postprocess_command(self, event): + msgid = TAG.get_value(event["line"].tags) + if msgid: + event["stdout"].tags["+draft/reply"] = msgid + event["stderr"].tags["+draft/reply"] = msgid diff --git a/modules/ircv3_typing.py b/modules/ircv3_typing.py new file mode 100644 index 00000000..ca2fce2b --- /dev/null +++ b/modules/ircv3_typing.py @@ -0,0 +1,27 @@ +from src import IRCLine, ModuleManager, utils + +CAP = utils.irc.Capability("message-tags", "draft/message-tags-0.2") + +class Module(ModuleManager.BaseModule): + def _tagmsg(self, target, state): + return IRCLine.ParsedLine("TAGMSG", [target], + tags={"+draft/typing": state}) + def _has_tags(self, server): + return server.has_capability(CAP) + + @utils.hook("preprocess.command") + def preprocess(self, event): + if (self._has_tags(event["server"]) and + event["hook"].get_kwarg("expect_output", True)): + event["target"]._typing = True + event["server"].send(self._tagmsg(event["target_str"], "active"), + immediate=True) + else: + event["target"]._typing = False + + @utils.hook("postprocess.command") + def postprocess(self, event): + if (event["target"]._typing and + not event["stdout"].has_text() and + not event["stderr"].has_text()): + event["server"].send(self._tagmsg(event["target_str"], "done")) diff --git a/src/ModuleManager.py b/src/ModuleManager.py index 50af3493..5ef57b73 100644 --- a/src/ModuleManager.py +++ b/src/ModuleManager.py @@ -88,10 +88,12 @@ class ModuleDefinition(object): class LoadedModule(object): def __init__(self, name: str, + title: str, module: BaseModule, context: str, import_name: str): self.name = name + self.title = title self.module = module self.context = context self.import_name = import_name @@ -233,8 +235,8 @@ class ModuleManager(object): module_object = module_object_pointer(bot, context_events, context_exports, context_timers, self.log) - if not hasattr(module_object, "_name"): - module_object._name = definition.name.title() + module_title = (getattr(module_object, "_name", None) or + definition.name.title()) # @utils.hook() magic for attribute_name in dir(module_object): @@ -256,8 +258,8 @@ class ModuleManager(object): raise ModuleNameCollisionException("Module name '%s' " "attempted to be used twice" % definition.name) - return LoadedModule(definition.name, module_object, context, - import_name) + return LoadedModule(definition.name, module_title, module_object, + context, import_name) def load_module(self, bot: "IRCBot.Bot", definition: ModuleDefinition ) -> LoadedModule: