refactor commands (mostly stdout/stderr) and split typing/reply out
closes #208
This commit is contained in:
parent
bcb2cc73f9
commit
5730d6fe59
5 changed files with 119 additions and 158 deletions
|
@ -8,9 +8,10 @@ from . import outs
|
||||||
COMMAND_METHOD = "command-method"
|
COMMAND_METHOD = "command-method"
|
||||||
COMMAND_METHODS = ["PRIVMSG", "NOTICE"]
|
COMMAND_METHODS = ["PRIVMSG", "NOTICE"]
|
||||||
|
|
||||||
MESSAGE_TAGS_CAP = utils.irc.Capability("message-tags",
|
STR_MORE = " (more...)"
|
||||||
"draft/message-tags-0.2")
|
STR_MORE_LEN = len(STR_MORE.encode("utf8"))
|
||||||
MSGID_TAG = utils.irc.MessageTag("msgid", "draft/msgid")
|
STR_CONTINUED = "(...continued)"
|
||||||
|
WORD_BOUNDARIES = [" "]
|
||||||
|
|
||||||
NON_ALPHANUMERIC = [char for char in string.printable if not char.isalnum()]
|
NON_ALPHANUMERIC = [char for char in string.printable if not char.isalnum()]
|
||||||
|
|
||||||
|
@ -48,8 +49,6 @@ class Module(ModuleManager.BaseModule):
|
||||||
target = event["user"]
|
target = event["user"]
|
||||||
else:
|
else:
|
||||||
target = event["channel"]
|
target = event["channel"]
|
||||||
target.last_stdout = None
|
|
||||||
target.last_stderr = None
|
|
||||||
|
|
||||||
def has_command(self, command):
|
def has_command(self, command):
|
||||||
return command.lower() in self.events.on("received").on(
|
return command.lower() in self.events.on("received").on(
|
||||||
|
@ -62,10 +61,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
if s and s[-1] in [":", ","]:
|
if s and s[-1] in [":", ","]:
|
||||||
return server.is_own_nickname(s[:-1])
|
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,
|
return target.get_setting(COMMAND_METHOD,
|
||||||
server.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):
|
def _find_command_hook(self, server, target, is_channel, command, args):
|
||||||
if not self.has_command(command):
|
if not self.has_command(command):
|
||||||
|
@ -159,31 +158,13 @@ class Module(ModuleManager.BaseModule):
|
||||||
if not is_success:
|
if not is_success:
|
||||||
raise utils.EventError("%s: %s" % (user.nickname, message))
|
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,
|
def command(self, server, target, target_str, is_channel, user, command,
|
||||||
args_split, tags, hook, **kwargs):
|
args_split, line, hook, **kwargs):
|
||||||
message_tags = server.has_capability(MESSAGE_TAGS_CAP)
|
module_name = (self._get_prefix(hook) or
|
||||||
expect_output = hook.get_kwarg("expect_output", True)
|
self.bot.modules.from_context(hook.context).title)
|
||||||
|
|
||||||
module_name = self._get_prefix(hook) or ""
|
stdout = outs.StdOut(module_name)
|
||||||
if not module_name and hasattr(hook.function, "__self__"):
|
stderr = outs.StdOut(module_name)
|
||||||
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)
|
|
||||||
|
|
||||||
ret = False
|
ret = False
|
||||||
has_out = False
|
has_out = False
|
||||||
|
@ -192,9 +173,9 @@ class Module(ModuleManager.BaseModule):
|
||||||
args_split = list(filter(None, args_split))
|
args_split = list(filter(None, args_split))
|
||||||
|
|
||||||
event_kwargs = {"hook": hook, "user": user, "server": server,
|
event_kwargs = {"hook": hook, "user": user, "server": server,
|
||||||
"target": target, "is_channel": is_channel, "tags": tags,
|
"target": target, "target_str": target_str,
|
||||||
"args_split": args_split, "command": command,
|
"is_channel": is_channel, "line": line, "args_split": args_split,
|
||||||
"args": " ".join(args_split), "stdout": stdout,
|
"command": command, "args": " ".join(args_split), "stdout": stdout,
|
||||||
"stderr": stderr}
|
"stderr": stderr}
|
||||||
event_kwargs.update(kwargs)
|
event_kwargs.update(kwargs)
|
||||||
|
|
||||||
|
@ -203,37 +184,59 @@ class Module(ModuleManager.BaseModule):
|
||||||
event_kwargs["check_assert"] = check_assert
|
event_kwargs["check_assert"] = check_assert
|
||||||
|
|
||||||
check_success, check_message = self._check("preprocess", event_kwargs)
|
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:
|
if check_message:
|
||||||
stderr.write("%s: %s" % (user.nickname, check_message)
|
stderr.write("%s: %s" % (user.nickname, check_message))
|
||||||
).send(command_method)
|
|
||||||
return True
|
|
||||||
|
|
||||||
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:
|
@utils.hook("postprocess.command")
|
||||||
hook.call(new_event)
|
@utils.kwarg("priority", EventManager.PRIORITY_LOW)
|
||||||
except utils.EventError as e:
|
def postprocess(self, event):
|
||||||
stderr.write(str(e)).send(command_method)
|
color = None
|
||||||
return True
|
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):
|
line_str = "[%s] %s" % (utils.irc.color(obj.prefix, color), obj.pop())
|
||||||
has_out = stdout.has_text() or stderr.has_text()
|
method = self._command_method(event["server"], event["target"])
|
||||||
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
|
|
||||||
|
|
||||||
if expect_output and message_tags and not has_out:
|
if not method in ["PRIVMSG", "NOTICE"]:
|
||||||
line = self._tagmsg(target_str, {"+draft/typing": "done"})
|
raise ValueError("Unknown command-method '%s'" % method)
|
||||||
server.send(line, immediate=True)
|
|
||||||
|
|
||||||
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")
|
@utils.hook("preprocess.command")
|
||||||
def _check_min_args(self, event):
|
def _check_min_args(self, event):
|
||||||
|
@ -293,7 +296,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
if hook:
|
if hook:
|
||||||
self.command(event["server"], event["channel"],
|
self.command(event["server"], event["channel"],
|
||||||
event["target_str"], True, event["user"], command,
|
event["target_str"], True, event["user"], command,
|
||||||
args_split, event["tags"], hook,
|
args_split, event["line"], hook,
|
||||||
command_prefix=command_prefix)
|
command_prefix=command_prefix)
|
||||||
else:
|
else:
|
||||||
self.events.on("unknown.command").call(server=event["server"],
|
self.events.on("unknown.command").call(server=event["server"],
|
||||||
|
@ -313,7 +316,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
command = hook.get_kwarg("command", "")
|
command = hook.get_kwarg("command", "")
|
||||||
res = self.command(event["server"], event["channel"],
|
res = self.command(event["server"], event["channel"],
|
||||||
event["target_str"], True, event["user"], command,
|
event["target_str"], True, event["user"], command,
|
||||||
"", event["tags"], hook, match=match,
|
"", event["line"], hook, match=match,
|
||||||
message=event["message"], command_prefix="",
|
message=event["message"], command_prefix="",
|
||||||
action=event["action"])
|
action=event["action"])
|
||||||
|
|
||||||
|
@ -344,7 +347,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
if hook:
|
if hook:
|
||||||
self.command(event["server"], event["user"],
|
self.command(event["server"], event["user"],
|
||||||
event["user"].nickname, False, event["user"], command,
|
event["user"].nickname, False, event["user"], command,
|
||||||
args_split, event["tags"], hook, command_prefix="")
|
args_split, event["line"], hook, command_prefix="")
|
||||||
else:
|
else:
|
||||||
self.events.on("unknown.command").call(server=event["server"],
|
self.events.on("unknown.command").call(server=event["server"],
|
||||||
target=event["user"], user=event["user"], command=command,
|
target=event["user"], user=event["user"], command=command,
|
||||||
|
@ -364,15 +367,6 @@ class Module(ModuleManager.BaseModule):
|
||||||
def _get_alias_of(self, hook):
|
def _get_alias_of(self, hook):
|
||||||
return hook.get_kwarg("alias_of", None)
|
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")
|
@utils.hook("send.stdout")
|
||||||
def send_stdout(self, event):
|
def send_stdout(self, event):
|
||||||
target = event["target"]
|
target = event["target"]
|
||||||
|
|
|
@ -1,98 +1,29 @@
|
||||||
import re
|
import re
|
||||||
from src import IRCLine, utils
|
from src import IRCLine, utils
|
||||||
|
|
||||||
STR_MORE = " (more...)"
|
class StdOut(object):
|
||||||
STR_MORE_LEN = len(STR_MORE.encode("utf8"))
|
def __init__(self, prefix):
|
||||||
STR_CONTINUED = "(...continued) "
|
self.prefix = prefix
|
||||||
WORD_BOUNDARY = ' '
|
self._lines = []
|
||||||
|
self.tags = {}
|
||||||
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
|
|
||||||
self._assured = False
|
self._assured = False
|
||||||
|
|
||||||
def assure(self):
|
def assure(self):
|
||||||
self._assured = True
|
self._assured = True
|
||||||
|
|
||||||
def write(self, text):
|
def write(self, text):
|
||||||
self._text += text
|
self.write_lines(
|
||||||
self.written = True
|
text.replace("\r", "").replace("\n\n", "\n").split("\n"))
|
||||||
return self
|
def write_lines(self, lines):
|
||||||
def writeline(self, line):
|
self._lines += list(filter(None, lines))
|
||||||
self._text += "%s\n" % line
|
|
||||||
|
|
||||||
def send(self, method):
|
def get_all(self):
|
||||||
if self.has_text():
|
return self._lines.copy()
|
||||||
prefix = ""
|
def pop(self):
|
||||||
if not self._hide_prefix:
|
return self._lines.pop(0)
|
||||||
prefix = utils.consts.RESET + "[%s] " % self._prefix
|
def insert(self, text):
|
||||||
|
self._lines.insert(0, text)
|
||||||
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 has_text(self):
|
def has_text(self):
|
||||||
return bool(self._text)
|
return bool(self._lines)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
|
@ -22,3 +22,10 @@ class Module(ModuleManager.BaseModule):
|
||||||
def ctcp(self, event):
|
def ctcp(self, event):
|
||||||
if event["is_channel"]:
|
if event["is_channel"]:
|
||||||
self._on_channel(event["target"], event["tags"])
|
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
|
||||||
|
|
27
modules/ircv3_typing.py
Normal file
27
modules/ircv3_typing.py
Normal file
|
@ -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"))
|
|
@ -88,10 +88,12 @@ class ModuleDefinition(object):
|
||||||
class LoadedModule(object):
|
class LoadedModule(object):
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
name: str,
|
name: str,
|
||||||
|
title: str,
|
||||||
module: BaseModule,
|
module: BaseModule,
|
||||||
context: str,
|
context: str,
|
||||||
import_name: str):
|
import_name: str):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.title = title
|
||||||
self.module = module
|
self.module = module
|
||||||
self.context = context
|
self.context = context
|
||||||
self.import_name = import_name
|
self.import_name = import_name
|
||||||
|
@ -233,8 +235,8 @@ class ModuleManager(object):
|
||||||
module_object = module_object_pointer(bot, context_events,
|
module_object = module_object_pointer(bot, context_events,
|
||||||
context_exports, context_timers, self.log)
|
context_exports, context_timers, self.log)
|
||||||
|
|
||||||
if not hasattr(module_object, "_name"):
|
module_title = (getattr(module_object, "_name", None) or
|
||||||
module_object._name = definition.name.title()
|
definition.name.title())
|
||||||
|
|
||||||
# @utils.hook() magic
|
# @utils.hook() magic
|
||||||
for attribute_name in dir(module_object):
|
for attribute_name in dir(module_object):
|
||||||
|
@ -256,8 +258,8 @@ class ModuleManager(object):
|
||||||
raise ModuleNameCollisionException("Module name '%s' "
|
raise ModuleNameCollisionException("Module name '%s' "
|
||||||
"attempted to be used twice" % definition.name)
|
"attempted to be used twice" % definition.name)
|
||||||
|
|
||||||
return LoadedModule(definition.name, module_object, context,
|
return LoadedModule(definition.name, module_title, module_object,
|
||||||
import_name)
|
context, import_name)
|
||||||
|
|
||||||
def load_module(self, bot: "IRCBot.Bot", definition: ModuleDefinition
|
def load_module(self, bot: "IRCBot.Bot", definition: ModuleDefinition
|
||||||
) -> LoadedModule:
|
) -> LoadedModule:
|
||||||
|
|
Loading…
Reference in a new issue