From dfde4c5f49d27f496382b59ac0f1260cfdbf69e4 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 5 Oct 2014 18:19:20 +0200 Subject: [PATCH] New message processing --- bot.py | 43 ++++----- consumer.py | 45 ++------- exception.py | 9 +- hooks/messagehook.py | 37 +++---- importer.py | 39 ++++---- message.py | 71 -------------- message/__init__.py | 160 +++++++++++++++++++++++++++++++ message/printer/IRC.py | 69 +++++++++++++ message/printer/__init__.py | 0 message/visitor.py | 25 +++++ modules/alias.py | 48 ++++------ modules/events.py | 26 ++--- modules/mediawiki.py | 6 +- modules/more.py | 100 +++++++++---------- modules/watchWebsite/__init__.py | 4 +- modules/worldcup.py | 1 - modules/ycc.py | 13 ++- prompt/builtins.py | 4 +- server/IRC.py | 131 +++++++++++++++---------- server/__init__.py | 15 +++ 20 files changed, 520 insertions(+), 326 deletions(-) delete mode 100644 message.py create mode 100644 message/__init__.py create mode 100644 message/printer/IRC.py create mode 100644 message/printer/__init__.py create mode 100644 message/visitor.py diff --git a/bot.py b/bot.py index e04420e..b136b26 100644 --- a/bot.py +++ b/bot.py @@ -25,7 +25,7 @@ import threading import time import uuid -__version__ = '3.4.dev0' +__version__ = '3.4.dev1' __author__ = 'nemunaire' from consumer import Consumer, EventConsumer, MessageConsumer @@ -33,7 +33,7 @@ from event import ModuleEvent from hooks.messagehook import MessageHook from hooks.manager import HooksManager from networkbot import NetworkBot -from server.IRC import IRCServer +from server.IRC import IRC as IRCServer from server.DCC import DCC logger = logging.getLogger("nemubot.bot") @@ -74,28 +74,27 @@ class Bot(threading.Thread): # Own hooks self.hooks = HooksManager() def in_ping(msg): - if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.text, re.I) is not None: - return "PRIVMSG %s :%s: pong" % (",".join(msg.receivers), msg.nick) - self.hooks.add_hook(MessageHook(in_ping), "in", "PRIVMSG", "ask") + if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.message, re.I) is not None: + return msg.respond("pong") + self.hooks.add_hook(MessageHook(in_ping), "in", "DirectAsk") def _help_msg(msg): """Parse and response to help messages""" - cmd = msg.cmds from more import Response - res = Response() - if len(cmd) > 1: - if cmd[1] in self.modules: - if len(cmd) > 2: - if hasattr(self.modules[cmd[1]], "HELP_cmd"): - res.append_message(self.modules[cmd[1]].HELP_cmd(cmd[2])) + res = Response(channel=msg.frm) + if len(msg.args) > 1: + if msg.args[0] in self.modules: + if len(msg.args) > 2: + if hasattr(self.modules[msg.args[0]], "HELP_cmd"): + res.append_message(self.modules[msg.args[0]].HELP_cmd(msg.args[1])) else: - res.append_message("No help for command %s in module %s" % (cmd[2], cmd[1])) - elif hasattr(self.modules[cmd[1]], "help_full"): - res.append_message(self.modules[cmd[1]].help_full()) + res.append_message("No help for command %s in module %s" % (msg.args[1], msg.args[0])) + elif hasattr(self.modules[msg.args[0]], "help_full"): + res.append_message(self.modules[msg.args[0]].help_full()) else: - res.append_message("No help for module %s" % cmd[1]) + res.append_message("No help for module %s" % msg.args[0]) else: - res.append_message("No module named %s" % cmd[1]) + res.append_message("No module named %s" % msg.args[0]) else: res.append_message("Pour me demander quelque chose, commencez " "votre message par mon nom ; je réagis " @@ -113,7 +112,7 @@ class Bot(threading.Thread): " de tous les modules disponibles localement", message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im].__doc__) for im in self.modules if self.modules[im].__doc__]) return res - self.hooks.add_hook(MessageHook(_help_msg, "help"), "in", "PRIVMSG", "cmd") + self.hooks.add_hook(MessageHook(_help_msg, "help"), "in", "Command") # Other known bots, making a bots network self.network = dict() @@ -343,12 +342,10 @@ class Bot(threading.Thread): """Add a module to the context, if already exists, unload the old one before""" # Check if the module already exists - for mod in self.modules.keys(): - if self.modules[mod].name == module.name: - self.unload_module(self.modules[mod].name) - break + if module.__name__ in self.modules: + self.unload_module(module.__name__) - self.modules[module.name] = module + self.modules[module.__name__] = module return True diff --git a/consumer.py b/consumer.py index 17283aa..72e3635 100644 --- a/consumer.py +++ b/consumer.py @@ -20,13 +20,6 @@ import logging import queue import re import threading -import traceback -import sys - -import bot -from server.DCC import DCC -from message import Message -import server logger = logging.getLogger("nemubot.consumer") @@ -48,26 +41,10 @@ class MessageConsumer: msg -- The Message or Response to qualify """ - if not hasattr(msg, "qual") or msg.qual is None: - # Assume this is a message with no particulariry - msg.qual = "def" - # Define the source server if not already done if not hasattr(msg, "server") or msg.server is None: msg.server = self.srv.id - if isinstance(msg, Message): - if msg.cmd == "PRIVMSG" or msg.cmd == "NOTICE": - msg.is_owner = (msg.nick == self.srv.owner) - msg.private = msg.private or (len(msg.receivers) == 1 and msg.receivers[0] == self.srv.nick) - if msg.private: - msg.qual = "ask" - - # Remove nemubot: - if msg.qual != "cmd" and msg.text.find(self.srv.nick) == 0 and len(msg.text) > len(self.srv.nick) + 2 and msg.text[len(self.srv.nick)] == ":": - msg.text = msg.text[len(self.srv.nick) + 1:].strip() - msg.qual = "ask" - return msg @@ -84,8 +61,8 @@ class MessageConsumer: while len(new_msg) > 0: msg = new_msg.pop(0) - for h in hm.get_hooks("pre", msg.cmd, msg.qual): - if h.match(message=msg, server=self.srv): + for h in hm.get_hooks("pre", type(msg).__name__): + if h.match(msg, server=self.srv): res = h.run(msg) if isinstance(res, list): for i in range(len(res)): @@ -113,17 +90,12 @@ class MessageConsumer: self.responses = list() for msg in self.msgs: - for h in hm.get_hooks("in", msg.cmd, msg.qual): - if h.match(message=msg, server=self.srv): + for h in hm.get_hooks("in", type(msg).__name__): + if h.match(msg, server=self.srv): res = h.run(msg) if isinstance(res, list): - for r in res: - if hasattr(r, "set_sender"): - r.set_sender(msg.sender) self.responses += res elif res is not None: - if hasattr(res, "set_sender"): - res.set_sender(msg.sender) self.responses.append(res) @@ -145,7 +117,7 @@ class MessageConsumer: continue msg = self.first_treat(ff) for h in hm.get_hooks("post"): - if h.match(message=msg, server=self.srv): + if h.match(msg, server=self.srv): res = h.run(msg) if isinstance(res, list): for i in range(len(res)): @@ -154,6 +126,7 @@ class MessageConsumer: break msg = None new_msg += res + break elif res is not None and res != msg: new_msg.append(res) msg = None @@ -161,6 +134,8 @@ class MessageConsumer: elif res is None or res == False: msg = None break + else: + msg = res if msg is not None: self.responses.append(msg) @@ -182,7 +157,7 @@ class MessageConsumer: if self.responses is not None and len(self.responses) > 0: self.post_treat(context.hooks) except: - logger.exception("Error occurred during the processing of the message: %s", self.msgs[0].raw) + logger.exception("Error occurred during the processing of the %s: %s", type(self.msgs[0]).__name__, self.msgs[0]) for res in self.responses: to_server = None @@ -200,7 +175,7 @@ class MessageConsumer: continue # Sent the message only if treat_post authorize it - to_server.write(res) + to_server.send_response(res) class EventConsumer: """Store a event before treating""" diff --git a/exception.py b/exception.py index cfc721b..92229bf 100644 --- a/exception.py +++ b/exception.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from message import TextMessage, DirectAsk + class IRCException(Exception): def __init__(self, message, personnal=True): @@ -24,6 +26,7 @@ class IRCException(Exception): self.personnal = personnal def fill_response(self, msg): - # TODO: no more Response usable here - from more import Response - return Response(self.message, channel=msg.receivers, nick=(msg.nick if self.personnal else None)) + if self.personnal: + return DirectAsk(msg.frm, self.message, server=msg.server, to=msg.to_response) + else: + return TextMessage(self.message, server=msg.server, to=msg.to_response) diff --git a/hooks/messagehook.py b/hooks/messagehook.py index 0055d04..1ab09c6 100644 --- a/hooks/messagehook.py +++ b/hooks/messagehook.py @@ -20,8 +20,7 @@ import re from exception import IRCException import hooks - -from message import Message +import message class MessageHook(hooks.AbstractHook): @@ -39,24 +38,28 @@ class MessageHook(hooks.AbstractHook): self.channels = channels - def match(self, message, server=None): - if not isinstance(message, Message): + def match(self, msg, server=None): + if not isinstance(msg, message.AbstractMessage): return True - elif message.qual == "cmd": - return self.is_matching(message.cmds[0], message.channel, server) - elif hasattr(message, "text"): - return self.is_matching(message.text, message.channel, server) - elif len(message.params) > 0: - return self.is_matching(message.params[0], message.channel, server) + elif isinstance(msg, message.Command): + return self.is_matching(msg.cmd, msg.to, server) + elif isinstance(msg, message.TextMessage): + return self.is_matching(msg.message, msg.to, server) else: - return self.is_matching(message.cmd, message.channel, server) + return False - def is_matching(self, strcmp, channel=None, server=None): + def is_matching(self, strcmp, receivers=list(), server=None): """Test if the current hook correspond to the message""" - return (channel is None or len(self.channels) <= 0 or - channel in self.channels) and (server is None or - self.server is None or self.server == server) and ( - (self.name is None or strcmp == self.name) and ( - self.regexp is None or re.match(self.regexp, strcmp))) + if (server is None or self.server is None or self.server == server + ) and ((self.name is None or strcmp == self.name) and ( + self.regexp is None or re.match(self.regexp, strcmp))): + + if receivers and self.channels: + for receiver in receivers: + if receiver in self.channels: + return True + else: + return True + return False diff --git a/importer.py b/importer.py index ddee457..b01848c 100644 --- a/importer.py +++ b/importer.py @@ -28,6 +28,7 @@ from bot import __version__ import event import exception import hooks +from message import TextMessage import xmlparser logger = logging.getLogger("nemubot.importer") @@ -124,13 +125,6 @@ class ModuleLoader(SourceLoader): def load_module(self, fullname): module = self._load_module(fullname, sourceless=True) - # Remove the module from sys list - del sys.modules[fullname] - - # If the module was already loaded, then reload it - if hasattr(module, '__LOADED__'): - reload(module) - # Check that is a valid nemubot module if not hasattr(module, "nemubotversion"): raise ImportError("Module `%s' is not a nemubot module."%self.name) @@ -144,21 +138,25 @@ class ModuleLoader(SourceLoader): module.logger = logging.getLogger("nemubot.module." + fullname) def prnt(*args): - print("[%s]" % module.name, *args) + print("[%s]" % module.__name__, *args) module.logger.info(*args) def prnt_dbg(*args): if module.DEBUG: - print("{%s}" % module.name, *args) + print("{%s}" % module.__name__, *args) module.logger.debug(*args) def mod_save(): - fpath = self.context.data_path + "/" + module.name + ".xml" + fpath = self.context.data_path + "/" + module.__name__ + ".xml" module.print_debug("Saving DATAS to " + fpath) module.DATAS.save(fpath) def send_response(server, res): if server in self.context.servers: - return self.context.servers[server].write("PRIVMSG %s :%s" % (",".join(res.receivers), res.get_message())) + r = res.next_response() + if r.server is not None: + return self.context.servers[r.server].send_response(r) + else: + return self.context.servers[server].send_response(r) else: module.logger.error("Try to send a message to the unknown server: %s", server) return False @@ -183,7 +181,6 @@ class ModuleLoader(SourceLoader): module.REGISTERED_EVENTS = list() module.DEBUG = False module.DIR = self.mpath - module.name = fullname module.print = prnt module.print_debug = prnt_dbg module.send_response = send_response @@ -195,7 +192,7 @@ class ModuleLoader(SourceLoader): if not hasattr(module, "NODATA"): module.DATAS = xmlparser.parse_file(self.context.data_path - + module.name + ".xml") + + module.__name__ + ".xml") module.save = mod_save else: module.DATAS = None @@ -216,7 +213,7 @@ class ModuleLoader(SourceLoader): break if depend["name"] not in module.MODS: logger.error("In module `%s', module `%s' require by this " - "module but is not loaded.", module.name, + "module but is not loaded.", module.__name__, depend["name"]) return @@ -230,21 +227,21 @@ class ModuleLoader(SourceLoader): # Register hooks register_hooks(module, self.context, self.prompt) - logger.info("Module '%s' successfully loaded.", module.name) + logger.info("Module '%s' successfully loaded.", module.__name__) else: - logger.error("An error occurs while importing `%s'.", module.name) + logger.error("An error occurs while importing `%s'.", module.__name__) raise ImportError("An error occurs while importing `%s'." - % module.name) + % module.__name__) return module def convert_legacy_store(old): if old == "cmd_hook" or old == "cmd_rgxp" or old == "cmd_default": - return "in_PRIVMSG_cmd" + return "in_Command" elif old == "ask_hook" or old == "ask_rgxp" or old == "ask_default": - return "in_PRIVMSG_ask" + return "in_DirectAsk" elif old == "msg_hook" or old == "msg_rgxp" or old == "msg_default": - return "in_PRIVMSG_def" + return "in_TextMessage" elif old == "all_post": return "post" elif old == "all_pre": @@ -258,7 +255,7 @@ def add_cap_hook(prompt, module, cmd): prompt.add_cap_hook(cmd["name"], getattr(module, cmd["call"])) else: logger.warn("In module `%s', no function `%s' defined for `%s' " - "command hook.", module.name, cmd["call"], cmd["name"]) + "command hook.", module.__name__, cmd["call"], cmd["name"]) def register_hooks(module, context, prompt): """Register all available hooks""" diff --git a/message.py b/message.py deleted file mode 100644 index 3f6673d..0000000 --- a/message.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from datetime import datetime -import shlex - -class Message: - def __init__ (self, orig, private=False): - self.cmd = orig.cmd - self.tags = orig.tags - self.params = orig.params - self.private = private - self.prefix = orig.prefix - self.nick = orig.nick - - # Special commands - if self.cmd == 'PRIVMSG' or self.cmd == 'NOTICE': - self.receivers = orig.decode(self.params[0]).split(',') - - # If CTCP, remove 0x01 - if len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01): - self.is_ctcp = True - self.text = orig.decode(self.params[1][1:len(self.params[1])-1]) - else: - self.is_ctcp = False - self.text = orig.decode(self.params[1]) - - # Split content by words - self.parse_content() - - else: - for i in range(0, len(self.params)): - self.params[i] = orig.decode(self.params[i]) - - - # TODO: here for legacy content - @property - def sender(self): - return self.prefix - @property - def channel(self): - return self.receivers[0] - - - def parse_content(self): - """Parse or reparse the message content""" - # Remove ! - if len(self.text) > 1 and self.text[0] == '!': - self.qual = "cmd" - self.text = self.text[1:].strip() - - # Split content by words - try: - self.cmds = shlex.split(self.text) - except ValueError: - self.cmds = self.text.split(' ') diff --git a/message/__init__.py b/message/__init__.py new file mode 100644 index 0000000..2b69b01 --- /dev/null +++ b/message/__init__.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from datetime import datetime, timezone + +class AbstractMessage: + + """This class represents an abstract message""" + + def __init__(self, server, date=None, to=None, to_response=None, frm=None): + """Initialize an abstract message + + Arguments: + server -- the servir identifier + date -- time of the message reception, default: now + to -- list of recipients + to_response -- if channel(s) where send the response differ + frm -- the sender + """ + + self.server = server + self.date = datetime.now(timezone.utc) if date is None else date + self.to = to if to is not None else list() + self._to_response = to_response if to_response is None or isinstance(to_response, list) else [ to_response ] + self.frm = frm # None allowed when it designate this bot + + + @property + def to_response(self): + if self._to_response is not None: + return self._to_response + else: + return self.to + + + @property + def receivers(self): + # TODO: this is for legacy modules + return self.to_response + + @property + def channel(self): + # TODO: this is for legacy modules + return self.to_response[0] + + @property + def nick(self): + # TODO: this is for legacy modules + return self.frm + + + def accept(self, visitor): + visitor.visit(self) + + + def export_args(self, without=list()): + if not isinstance(without, list): + without = [ without ] + + ret = { + "server": self.server, + "date": self.date, + "to": self.to, + "to_response": self._to_response, + "frm": self.frm + } + + for w in without: + if w in ret: + del ret[w] + + return ret + + +class TextMessage(AbstractMessage): + + """This class represent a simple message send to someone""" + + def __init__(self, message, *args, **kargs): + """Initialize a message with no particular specificity + + Argument: + message -- the parsed message + """ + + AbstractMessage.__init__(self, *args, **kargs) + + self.message = message + + def __str__(self): + return self.message + + @property + def text(self): + # TODO: this is for legacy modules + return self.message + + +class DirectAsk(TextMessage): + + """This class represents a message to this bot""" + + def __init__(self, designated, *args, **kargs): + """Initialize a message to a specific person + + Argument: + designated -- the user designated by the message + """ + + TextMessage.__init__(self, *args, **kargs) + + self.designated = designated + + + def respond(self, message): + return DirectAsk(self.frm, + message, + server=self.server, + to=self.to_response) + + +class Command(AbstractMessage): + + """This class represents a specialized TextMessage""" + + def __init__(self, cmd, args=None, *nargs, **kargs): + AbstractMessage.__init__(self, *nargs, **kargs) + + self.cmd = cmd + self.args = args if args is not None else list() + + def __str__(self): + return self.cmd + " @" + ",@".join(self.args) + + @property + def cmds(self): + # TODO: this is for legacy modules + return [self.cmd] + self.args + + +class OwnerCommand(Command): + + """This class represents a special command incomming from the owner""" + + pass diff --git a/message/printer/IRC.py b/message/printer/IRC.py new file mode 100644 index 0000000..83155ac --- /dev/null +++ b/message/printer/IRC.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from message import TextMessage +from message.visitor import AbstractVisitor + +class IRC(AbstractVisitor): + + def __init__(self): + self.pp = "" + + + def visit_TextMessage(self, msg): + self.pp += "PRIVMSG %s :" % ",".join(msg.to) + if isinstance(msg.message, str): + self.pp += msg.message + else: + msg.message.accept(self) + self.pp += "\r\n" + + + def visit_DirectAsk(self, msg): + others = [to for to in msg.to if to != msg.designated] + + # Avoid nick starting message when discussing on user channel + if len(others) != len(msg.to): + res = TextMessage(msg.message, + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) + + if len(others): + res = TextMessage("%s: %s" % (msg.designated, msg.message), + server=msg.server, date=msg.date, + to=others, frm=msg.frm) + res.accept(self) + + + def visit_Command(self, msg): + res = TextMessage("!%s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) + + + def visit_OwnerCommand(self, msg): + res = TextMessage("`%s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) diff --git a/message/printer/__init__.py b/message/printer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/message/visitor.py b/message/visitor.py new file mode 100644 index 0000000..7328254 --- /dev/null +++ b/message/visitor.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class AbstractVisitor: + + def visit(self, obj): + """Visit a node""" + method_name = "visit_%s" % obj.__class__.__name__ + method = getattr(self, method_name) + return method(obj) diff --git a/modules/alias.py b/modules/alias.py index 370ac1d..314f545 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -5,8 +5,10 @@ import re import sys from datetime import datetime, timezone +import shlex from hooks import hook +from message import TextMessage, Command nemubotversion = 3.4 @@ -34,10 +36,8 @@ def set_variable(name, value, creator): DATAS.getNode("variables").addChild(var) def get_variable(name, msg=None): - if name == "sender": - return msg.sender - elif name == "nick": - return msg.nick + if name == "sender" or name == "from" or name == "nick": + return msg.frm elif name == "chan" or name == "channel": return msg.channel elif name == "date": @@ -141,35 +141,25 @@ def replace_variables(cnt, msg=None): return " ".join(cnt) -@hook("all_post") -def treat_variables(res): - for i in range(0, len(res.messages)): - if isinstance(res.messages[i], list): - res.messages[i] = replace_variables(", ".join(res.messages[i]), res) - else: - res.messages[i] = replace_variables(res.messages[i], res) - return res - -@hook("pre_PRIVMSG_cmd") +@hook("pre_Command") def treat_alias(msg): - if msg.cmds[0] in DATAS.getNode("aliases").index: - oldcmd = msg.cmds[0] - msg.text = msg.text.replace(msg.cmds[0], - DATAS.getNode("aliases").index[msg.cmds[0]]["origin"], 1) - - msg.qual = "def" - msg.parse_content() + if msg.cmd in DATAS.getNode("aliases").index: + txt = DATAS.getNode("aliases").index[msg.cmd]["origin"] + # TODO: for legacy compatibility + if txt[0] == "!": + txt = txt[1:] + try: + args = shlex.split(txt) + except ValueError: + args = txt.split(' ') + nmsg = Command(args[0], args[1:] + msg.args, **msg.export_args()) # Avoid infinite recursion - if oldcmd == msg.cmds[0]: - return msg - else: - return treat_alias(msg) + if msg.cmd != nmsg.cmd: + return nmsg + + return msg - else: - msg.text = replace_variables(msg.text, msg) - msg.parse_content() - return msg @hook("ask_default") def parseask(msg): diff --git a/modules/events.py b/modules/events.py index af20867..d0d9e6e 100644 --- a/modules/events.py +++ b/modules/events.py @@ -71,8 +71,7 @@ def start_countdown(msg): strnd["server"] = msg.server strnd["channel"] = msg.channel strnd["proprio"] = msg.nick - strnd["sender"] = msg.sender - strnd["start"] = msg.tags["time"] + strnd["start"] = msg.date strnd["name"] = msg.cmds[1] DATAS.addChild(strnd) @@ -84,7 +83,7 @@ def start_countdown(msg): result3 = re.match("(.*[^0-9])?([0-2]?[0-9]):([0-5]?[0-9])(:([0-5]?[0-9]))?", msg.cmds[2]) if result2 is not None or result3 is not None: try: - now = msg.tags["time"] + now = msg.date if result3 is None or result3.group(5) is None: sec = 0 else: sec = int(result3.group(5)) if result3 is None or result3.group(3) is None: minu = 0 @@ -109,7 +108,7 @@ def start_countdown(msg): raise IRCException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.cmds[1]) elif result1 is not None and len(result1) > 0: - strnd["end"] = msg.tags["time"] + strnd["end"] = msg.date for (t, g) in result1: if g is None or g == "" or g == "m" or g == "M": strnd["end"] += timedelta(minutes=int(t)) @@ -131,11 +130,13 @@ def start_countdown(msg): save() if "end" in strnd: return Response("%s commencé le %s et se terminera le %s." % - (msg.cmds[1], msg.tags["time"].strftime("%A %d %B %Y à %H:%M:%S"), - strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S"))) + (msg.cmds[1], msg.date.strftime("%A %d %B %Y à %H:%M:%S"), + strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S")), + nick=msg.frm) else: return Response("%s commencé le %s"% (msg.cmds[1], - msg.tags["time"].strftime("%A %d %B %Y à %H:%M:%S"))) + msg.date.strftime("%A %d %B %Y à %H:%M:%S")), + nick=msg.frm) @hook("cmd_hook", "end") @hook("cmd_hook", "forceend") @@ -145,7 +146,7 @@ def end_countdown(msg): if msg.cmds[1] in DATAS.index: if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.is_owner): - duration = countdown(msg.tags["time"] - DATAS.index[msg.cmds[1]].getDate("start")) + duration = countdown(msg.date - DATAS.index[msg.cmds[1]].getDate("start")) del_event(DATAS.index[msg.cmds[1]]["id"]) DATAS.delChild(DATAS.index[msg.cmds[1]]) save() @@ -182,9 +183,9 @@ def parseanswer(msg): if DATAS.index[msg.cmds[0]].name == "strend": if DATAS.index[msg.cmds[0]].hasAttribute("end"): - res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(msg.tags["time"] - DATAS.index[msg.cmds[0]].getDate("start")), countdown(DATAS.index[msg.cmds[0]].getDate("end") - msg.tags["time"]))) + res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(msg.date - DATAS.index[msg.cmds[0]].getDate("start")), countdown(DATAS.index[msg.cmds[0]].getDate("end") - msg.date))) else: - res.append_message("%s commencé il y a %s." % (msg.cmds[0], countdown(msg.tags["time"] - DATAS.index[msg.cmds[0]].getDate("start")))) + res.append_message("%s commencé il y a %s." % (msg.cmds[0], countdown(msg.date - DATAS.index[msg.cmds[0]].getDate("start")))) else: res.append_message(countdown_format(DATAS.index[msg.cmds[0]].getDate("start"), DATAS.index[msg.cmds[0]]["msg_before"], DATAS.index[msg.cmds[0]]["msg_after"])) return res @@ -222,7 +223,6 @@ def parseask(msg): evt["server"] = msg.server evt["channel"] = msg.channel evt["proprio"] = msg.nick - evt["sender"] = msg.sender evt["name"] = name.group(1) evt["start"] = extDate evt["msg_after"] = msg_after @@ -237,12 +237,12 @@ def parseask(msg): evt["server"] = msg.server evt["channel"] = msg.channel evt["proprio"] = msg.nick - evt["sender"] = msg.sender evt["name"] = name.group(1) evt["msg_before"] = texts.group (2) DATAS.addChild(evt) save() - return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1)) + return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1), + channel=msg.channel) else: raise IRCException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") diff --git a/modules/mediawiki.py b/modules/mediawiki.py index edb54e6..855238b 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -107,7 +107,7 @@ def search(site, term, ssl=False): web.striphtml(itm["snippet"].replace("", "\x03\x02").replace("", "\x03\x02"))) -@hook("in_PRIVMSG_cmd", "mediawiki") +@hook("cmd_hook", "mediawiki") def cmd_mediawiki(msg): """Read an article on a MediaWiki""" if len(msg.cmds) < 3: @@ -118,7 +118,7 @@ def cmd_mediawiki(msg): channel=msg.receivers) -@hook("in_PRIVMSG_cmd", "search_mediawiki") +@hook("cmd_hook", "search_mediawiki") def cmd_srchmediawiki(msg): """Search an article on a MediaWiki""" if len(msg.cmds) < 3: @@ -132,7 +132,7 @@ def cmd_srchmediawiki(msg): return res -@hook("in_PRIVMSG_cmd", "wikipedia") +@hook("cmd_hook", "wikipedia") def cmd_wikipedia(msg): if len(msg.cmds) < 3: raise IRCException("indicate a lang and a term to search") diff --git a/modules/more.py b/modules/more.py index 61dcdd4..4e09a5a 100644 --- a/modules/more.py +++ b/modules/more.py @@ -20,6 +20,7 @@ import logging +from message import TextMessage, DirectAsk from hooks import hook nemubotversion = 3.4 @@ -29,8 +30,7 @@ logger = logging.getLogger("nemubot.response") class Response: def __init__(self, message=None, channel=None, nick=None, server=None, nomore="No more message", title=None, more="(suite) ", - count=None, ctcp=False, shown_first_count=-1, - line_treat=None): + count=None, shown_first_count=-1, line_treat=None): self.nomore = nomore self.more = more self.line_treat = line_treat @@ -38,12 +38,10 @@ class Response: self.server = server self.messages = list() self.alone = True - self.is_ctcp = ctcp if message is not None: self.append_message(message, shown_first_count=shown_first_count) self.elt = 0 # Next element to display - self.sender = None self.channel = channel self.nick = nick self.count = count @@ -59,14 +57,6 @@ class Response: else: return [ self.channel ] - def set_sender(self, sender): - if sender is None or sender.find("!") < 0: - if sender is not None: - logger.warn("Bad sender provided in Response, it will be ignored.", stack_info=True) - self.sender = None - else: - self.sender = sender - def append_message(self, message, title=None, shown_first_count=-1): if type(message) is str: message = message.split('\n') @@ -120,13 +110,19 @@ class Response: if len(self.rawtitle) <= 0: self.rawtitle = None - def treat_ctcp(self, content): - if self.is_ctcp: - return "\x01" + content + "\x01" - else: - return content - def get_message(self): + def accept(self, visitor): + visitor.visit(self.next_response()) + + + def next_response(self, maxlen=440): + if self.nick: + return DirectAsk(self.nick, self.get_message(maxlen - len(self.nick) - 2), server=None, to=self.receivers) + else: + return TextMessage(self.get_message(maxlen), server=None, to=self.receivers) + + + def get_message(self, maxlen): if self.alone and len(self.messages) > 1: self.alone = False @@ -134,7 +130,7 @@ class Response: if hasattr(self.nomore, '__call__'): res = self.nomore(self) if res is None: - return self.treat_ctcp("No more message") + return "No more message" elif isinstance(res, Response): self.__dict__ = res.__dict__ elif isinstance(res, list): @@ -145,62 +141,59 @@ class Response: raise Exception("Type returned by nomore (%s) is not handled here." % type(res)) return self.get_message() else: - return self.treat_ctcp(self.nomore) + return self.nomore if self.line_treat is not None and self.elt == 0: self.messages[0] = self.line_treat(self.messages[0]).replace("\n", " ").strip() msg = "" - if self.channel is not None and self.nick is not None: - msg += self.nick + ": " - if self.title is not None: if self.elt > 0: msg += self.title + " " + self.more + ": " else: msg += self.title + ": " - if self.elt > 0: + elif self.elt > 0: msg += "[…] " elts = self.messages[0][self.elt:] if isinstance(elts, list): for e in elts: - if len(msg) + len(e) > 430: + if len(msg) + len(e) > maxlen - 3: msg += "[…]" self.alone = False - return self.treat_ctcp(msg) + return msg else: msg += e + ", " self.elt += 1 self.pop() - return self.treat_ctcp(msg[:len(msg)-2]) + return msg[:len(msg)-2] else: - if len(elts.encode()) <= 432: + if len(elts.encode()) <= maxlen: self.pop() if self.count is not None: - return self.treat_ctcp(msg + elts + (self.count % len(self.messages))) + return msg + elts + (self.count % len(self.messages)) else: - return self.treat_ctcp(msg + elts) + return msg + elts else: words = elts.split(' ') - if len(words[0].encode()) > 432 - len(msg.encode()): - self.elt += 432 - len(msg.encode()) - return self.treat_ctcp(msg + elts[:self.elt] + "[…]") + if len(words[0].encode()) > maxlen - len(msg.encode()): + self.elt += maxlen - len(msg.encode()) + return msg + elts[:self.elt] + "[…]" for w in words: - if len(msg.encode()) + len(w.encode()) > 431: + if len(msg.encode()) + len(w.encode()) >= maxlen: msg += "[…]" self.alone = False - return self.treat_ctcp(msg) + return msg else: msg += w + " " self.elt += len(w) + 1 self.pop() - return self.treat_ctcp(msg) + return msg SERVERS = dict() @@ -209,18 +202,17 @@ SERVERS = dict() def parseresponse(res): # TODO: handle inter-bot communication NOMORE # TODO: check that the response is not the one already saved - rstr = res.get_message() - - if not res.alone: + if isinstance(res, Response): if res.server not in SERVERS: SERVERS[res.server] = dict() for receiver in res.receivers: - SERVERS[res.server][receiver] = res - - ret = list() - for channel in res.receivers: - ret.append("%s %s :%s" % ("NOTICE" if res.is_ctcp else "PRIVMSG", channel, rstr)) - return ret + if receiver in SERVERS[res.server]: + nw, bk = SERVERS[res.server][receiver] + else: + nw, bk = None, None + if nw != res: + SERVERS[res.server][receiver] = (res, bk) + return res @hook("cmd_hook", "more") @@ -228,9 +220,13 @@ def cmd_more(msg): """Display next chunck of the message""" res = list() if msg.server in SERVERS: - for receiver in msg.receivers: + for receiver in msg.to_response: if receiver in SERVERS[msg.server]: - res.append(SERVERS[msg.server][receiver]) + nw, bk = SERVERS[msg.server][receiver] + if nw is not None and not nw.alone: + bk = nw + SERVERS[msg.server][receiver] = None, bk + res.append(bk) return res @@ -239,8 +235,12 @@ def cmd_next(msg): """Display the next information include in the message""" res = list() if msg.server in SERVERS: - for receiver in msg.receivers: + for receiver in msg.to_response: if receiver in SERVERS[msg.server]: - SERVERS[msg.server][receiver].pop() - res.append(SERVERS[msg.server][receiver]) + nw, bk = SERVERS[msg.server][receiver] + if nw is not None and not nw.alone: + bk = nw + SERVERS[msg.server][receiver] = None, bk + bk.pop() + res.append(bk) return res diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 3970ea7..2bd0d4c 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -56,7 +56,7 @@ def del_site(msg): site = DATAS.index[url] for a in site.getNodes("alert"): if a["channel"] == msg.channel: - if not (msg.sender == a["sender"] or msg.is_owner): + if not (msg.frm == a["nick"] or msg.is_owner): raise IRCException("vous ne pouvez pas supprimer cette URL.") site.delChild(a) if not site.hasNode("alert"): @@ -82,7 +82,7 @@ def add_site(msg, diffType="diff"): raise IRCException("je ne peux pas surveiller cette URL") alert = ModuleState("alert") - alert["sender"] = msg.sender + alert["nick"] = msg.nick alert["server"] = msg.server alert["channel"] = msg.channel alert["message"] = "{url} a changé !" diff --git a/modules/worldcup.py b/modules/worldcup.py index cd093ab..2426c7c 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -31,7 +31,6 @@ def start_watch(msg): w["server"] = msg.server w["channel"] = msg.channel w["proprio"] = msg.nick - w["sender"] = msg.sender w["start"] = datetime.now(timezone.utc) DATAS.addChild(w) save() diff --git a/modules/ycc.py b/modules/ycc.py index ca70432..6bf5ba8 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -8,11 +8,10 @@ from urllib.parse import quote from urllib.request import urlopen from hooks import hook +from message import TextMessage nemubotversion = 3.4 -from more import Response - def help_full(): return "!ycc []: with an argument, reduce the given thanks to ycc.fr; without argument, reduce the last URL said on the current channel." @@ -22,24 +21,28 @@ def gen_response(res, msg, srv): if res is None: raise IRCException("la situation est embarassante, il semblerait que YCC soit down :(") elif isinstance(res, str): - return Response("URL pour %s : %s" % (srv, res), msg.channel) + return TextMessage("URL pour %s : %s" % (srv, res), server=None, to=msg.to_response) else: raise IRCException("mauvaise URL : %s" % srv) @hook("cmd_hook", "ycc") def cmd_ycc(msg): + minify = list() + if len(msg.cmds) == 1: global LAST_URLS if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0: - msg.cmds.append(LAST_URLS[msg.channel].pop()) + minify.append(LAST_URLS[msg.channel].pop()) else: raise IRCException("je n'ai pas d'autre URL à réduire.") if len(msg.cmds) > 5: raise IRCException("je ne peux pas réduire autant d'URL d'un seul coup.") + else: + minify += msg.cmds[1:] res = list() - for url in msg.cmds[1:]: + for url in minify: o = urlparse(url, "http") if o.scheme != "": snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%@&=?") diff --git a/prompt/builtins.py b/prompt/builtins.py index ded17ea..6887852 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import imp import logging import os import xmlparser @@ -94,7 +95,8 @@ def load_file(filename, context): # Unexisting file, assume a name was passed, import the module! else: - __import__(filename) + tt = __import__(filename) + imp.reload(tt) def load(toks, context, prompt): diff --git a/server/IRC.py b/server/IRC.py index d040493..2410008 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -23,14 +23,15 @@ import time import shlex from channel import Channel -from message import Message -import server +import message +from message.printer.IRC import IRC as IRCPrinter from server.socket import SocketServer -class IRCServer(SocketServer): +class IRC(SocketServer): def __init__(self, node, nick, owner, realname): self.id = nick + "@" + node["host"] + ":" + node["port"] + self.printer = IRCPrinter SocketServer.__init__(self, node["host"], node["port"], @@ -60,18 +61,18 @@ class IRCServer(SocketServer): # Register CTCP capabilities self.ctcp_capabilities = dict() - def _ctcp_clientinfo(msg): + def _ctcp_clientinfo(msg, cmds): """Response to CLIENTINFO CTCP message""" - return _ctcp_response(" ".join(self.ctcp_capabilities.keys())) + return " ".join(self.ctcp_capabilities.keys()) - def _ctcp_dcc(msg): + def _ctcp_dcc(msg, cmds): """Response to DCC CTCP message""" try: - ip = srv.toIP(int(msg.cmds[3])) - port = int(msg.cmds[4]) + ip = srv.toIP(int(cmds[3])) + port = int(cmds[4]) conn = DCC(srv, msg.sender) except: - return _ctcp_response("ERRMSG invalid parameters provided as DCC CTCP request") + return "ERRMSG invalid parameters provided as DCC CTCP request" self.logger.info("Receive DCC connection request from %s to %s:%d", conn.sender, ip, port) @@ -80,27 +81,20 @@ class IRCServer(SocketServer): conn.send_dcc("Hello %s!" % conn.nick) else: self.logger.error("DCC: unable to connect to %s:%d", ip, port) - return _ctcp_response("ERRMSG unable to connect to %s:%d" % (ip, port)) + return "ERRMSG unable to connect to %s:%d" % (ip, port) import bot - self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive: %s" % msg.text) + self.ctcp_capabilities["ACTION"] = lambda msg, cmds: print ("ACTION receive: %s" % cmds) self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo #self.ctcp_capabilities["DCC"] = _ctcp_dcc - self.ctcp_capabilities["FINGER"] = lambda msg: _ctcp_response( - "VERSION nemubot v%s" % bot.__version__) - self.ctcp_capabilities["NEMUBOT"] = lambda msg: _ctcp_response( - "NEMUBOT %s" % bot.__version__) - self.ctcp_capabilities["PING"] = lambda msg: _ctcp_response( - "PING %s" % " ".join(msg.cmds[1:])) - self.ctcp_capabilities["SOURCE"] = lambda msg: _ctcp_response( - "SOURCE https://github.com/nemunaire/nemubot") - self.ctcp_capabilities["TIME"] = lambda msg: _ctcp_response( - "TIME %s" % (datetime.now())) - self.ctcp_capabilities["USERINFO"] = lambda msg: _ctcp_response( - "USERINFO %s" % self.realname) - self.ctcp_capabilities["VERSION"] = lambda msg: _ctcp_response( - "VERSION nemubot v%s" % bot.__version__) + self.ctcp_capabilities["FINGER"] = lambda msg, cmds: "VERSION nemubot v%s" % bot.__version__ + self.ctcp_capabilities["NEMUBOT"] = lambda msg, cmds: "NEMUBOT %s" % bot.__version__ + self.ctcp_capabilities["PING"] = lambda msg, cmds: "PING %s" % " ".join(cmds[1:]) + self.ctcp_capabilities["SOURCE"] = lambda msg, cmds: "SOURCE https://github.com/nemunaire/nemubot" + self.ctcp_capabilities["TIME"] = lambda msg, cmds: "TIME %s" % (datetime.now()) + self.ctcp_capabilities["USERINFO"] = lambda msg, cmds: "USERINFO %s" % self.realname + self.ctcp_capabilities["VERSION"] = lambda msg, cmds: "VERSION nemubot v%s" % bot.__version__ self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) @@ -190,6 +184,18 @@ class IRCServer(SocketServer): self.write("JOIN " + msg.decode(msg.params[1])) self.hookscmd["INVITE"] = _on_invite + # Handle CTCP requests + def _on_ctcp(msg): + if len(msg.params) != 2 or not msg.is_ctcp: return + cmds = msg.decode(msg.params[1][1:len(msg.params[1])-1]).split(' ') + if cmds[0] in self.ctcp_capabilities: + res = self.ctcp_capabilities[cmds[0]](msg, cmds) + else: + res = "ERRMSG Unknown or unimplemented CTCP request" + if res is not None: + self.write("NOTICE %s :\x01%s\x01" % (msg.nick, res)) + self.hookscmd["PRIVMSG"] = _on_ctcp + def _open(self): if SocketServer._open(self): @@ -215,31 +221,9 @@ class IRCServer(SocketServer): if msg.cmd in self.hookscmd: self.hookscmd[msg.cmd](msg) - else: - mes = msg.to_message() - mes.raw = msg.raw - - if hasattr(mes, "receivers"): - # Private message: prepare response - for i in range(len(mes.receivers)): - if mes.receivers[i] == self.nick: - mes.receivers[i] = mes.nick - - if (mes.cmd == "PRIVMSG" or mes.cmd == "NOTICE") and mes.is_ctcp: - if mes.cmds[0] in self.ctcp_capabilities: - res = self.ctcp_capabilities[mes.cmds[0]](mes) - else: - res = _ctcp_response("ERRMSG Unknown or unimplemented CTCP request") - if res is not None: - res = res % mes.nick - self.write(res) - - else: - yield mes - - -def _ctcp_response(msg): - return "NOTICE %%s :\x01%s\x01" % msg + mes = msg.to_message(self) + if mes is not None: + yield mes mgx = re.compile(b'''^(?:@(?P[^ ]+)\ )? @@ -303,6 +287,11 @@ class IRCMessage: self.tags[key] = value + @property + def is_ctcp(self): + return self.cmd == "PRIVMSG" and len(self.params) == 2 and len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01) + + def decode(self, s): """Decode the content string usign a specific encoding""" if isinstance(s, bytes): @@ -313,8 +302,6 @@ class IRCMessage: return s - def to_message(self): - return Message(self) def to_irc_string(self, client=True): """Pretty print the message to close to original input string @@ -336,3 +323,43 @@ class IRCMessage: res += " :" + self.decode(self.params[-1]) return res + + + def to_message(self, srv): + if self.cmd == "PRIVMSG" or self.cmd == "NOTICE": + + receivers = self.decode(self.params[0]).split(',') + + common_args = { + "server": srv.id, + "date": self.tags["time"], + "to": receivers, + "to_response": [r if r != srv.nick else self.nick for r in receivers], + "frm": self.nick + } + + # If CTCP, remove 0x01 + if self.is_ctcp: + text = self.decode(self.params[1][1:len(self.params[1])-1]) + else: + text = self.decode(self.params[1]) + + if len(text) > 1 and text[0] == '!': + text = text[1:].strip() + + # Split content by words + try: + args = shlex.split(text) + except ValueError: + args = text.split(' ') + + return message.Command(cmd=args[0], args=args[1:], **common_args) + + elif text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":": + text = text[len(srv.nick) + 1:].strip() + return message.DirectAsk(designated=srv.nick, message=text, **common_args) + + else: + return message.TextMessage(message=text, **common_args) + + return None diff --git a/server/__init__.py b/server/__init__.py index 28cf765..9c7ee55 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -89,3 +89,18 @@ class AbstractServer(io.IOBase): def exception(self): """Exception occurs in fd""" print("Unhandle file descriptor exception on server " + self.id) + + + def send_response(self, response): + """Send a formated Message class""" + if response is None: + return + + elif isinstance(response, list): + for r in response: + self.send_response(r) + + else: + vprnt = self.printer() + response.accept(vprnt) + self.write(vprnt.pp)