1
0
Fork 0

New message processing

This commit is contained in:
nemunaire 2014-10-05 18:19:20 +02:00
parent 981025610e
commit dfde4c5f49
20 changed files with 520 additions and 326 deletions

43
bot.py
View File

@ -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

View File

@ -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"""

View File

@ -16,6 +16,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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)

View File

@ -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

View File

@ -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"""

View File

@ -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 <http://www.gnu.org/licenses/>.
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(' ')

160
message/__init__.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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

69
message/printer/IRC.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

25
message/visitor.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
class AbstractVisitor:
def visit(self, obj):
"""Visit a node"""
method_name = "visit_%s" % obj.__class__.__name__
method = getattr(self, method_name)
return method(obj)

View File

@ -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):

View File

@ -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.")

View File

@ -107,7 +107,7 @@ def search(site, term, ssl=False):
web.striphtml(itm["snippet"].replace("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\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")

View File

@ -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

View File

@ -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é !"

View File

@ -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()

View File

@ -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 [<url>]: with an argument, reduce the given <url> 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, "/:%@&=?")

View File

@ -16,6 +16,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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):

View File

@ -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<tags>[^ ]+)\ )?
@ -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

View File

@ -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)