diff --git a/nemubot/message/printer/IRC.py b/nemubot/message/printer/IRC.py
deleted file mode 100644
index df9cb9f..0000000
--- a/nemubot/message/printer/IRC.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Nemubot is a smart and modulable IM bot.
-# Copyright (C) 2012-2016 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 nemubot.message import Text
-from nemubot.message.printer.socket import Socket as SocketPrinter
-
-
-class IRC(SocketPrinter):
-
- def visit_Text(self, msg):
- self.pp += "PRIVMSG %s :" % ",".join(msg.to)
- super().visit_Text(msg)
diff --git a/nemubot/message/printer/IRCLib.py b/nemubot/message/printer/IRCLib.py
new file mode 100644
index 0000000..abd1f2f
--- /dev/null
+++ b/nemubot/message/printer/IRCLib.py
@@ -0,0 +1,67 @@
+# Nemubot is a smart and modulable IM bot.
+# Copyright (C) 2012-2026 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 nemubot.message.visitor import AbstractVisitor
+
+
+class IRCLib(AbstractVisitor):
+
+ """Visitor that sends bot responses via an irc.client.ServerConnection.
+
+ Unlike the socket-based IRC printer (which builds a raw PRIVMSG string),
+ this calls connection.privmsg() directly so the library handles encoding,
+ line-length capping, and any internal locking.
+ """
+
+ def __init__(self, connection):
+ self._conn = connection
+
+ def _send(self, target, text):
+ try:
+ self._conn.privmsg(target, text)
+ except Exception:
+ pass # drop silently during reconnection
+
+ # Visitor methods
+
+ def visit_Text(self, msg):
+ if isinstance(msg.message, str):
+ for target in msg.to:
+ self._send(target, msg.message)
+ else:
+ msg.message.accept(self)
+
+ def visit_DirectAsk(self, msg):
+ text = msg.message if isinstance(msg.message, str) else str(msg.message)
+ # Mirrors socket.py logic:
+ # rooms that are NOT the designated nick get a "nick: " prefix
+ others = [to for to in msg.to if to != msg.designated]
+ if len(others) == 0 or len(others) != len(msg.to):
+ for target in msg.to:
+ self._send(target, text)
+ if others:
+ for target in others:
+ self._send(target, "%s: %s" % (msg.designated, text))
+
+ def visit_Command(self, msg):
+ parts = ["!" + msg.cmd] + list(msg.args)
+ for target in msg.to:
+ self._send(target, " ".join(parts))
+
+ def visit_OwnerCommand(self, msg):
+ parts = ["`" + msg.cmd] + list(msg.args)
+ for target in msg.to:
+ self._send(target, " ".join(parts))
diff --git a/nemubot/server/DCC.py b/nemubot/server/DCC.py
deleted file mode 100644
index f5d4b8f..0000000
--- a/nemubot/server/DCC.py
+++ /dev/null
@@ -1,239 +0,0 @@
-# Nemubot is a smart and modulable IM bot.
-# Copyright (C) 2012-2015 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 .
-
-import imp
-import os
-import re
-import socket
-import sys
-import time
-import threading
-import traceback
-
-import nemubot.message as message
-import nemubot.server as server
-
-#Store all used ports
-PORTS = list()
-
-class DCC(server.AbstractServer):
- def __init__(self, srv, dest, socket=None):
- super().__init__(name="Nemubot DCC server")
-
- self.error = False # An error has occur, closing the connection?
- self.messages = list() # Message queued before connexion
-
- # Informations about the sender
- self.sender = dest
- if self.sender is not None:
- self.nick = (self.sender.split('!'))[0]
- if self.nick != self.sender:
- self.realname = (self.sender.split('!'))[1]
- else:
- self.realname = self.nick
-
- # Keep the server
- self.srv = srv
- self.treatement = self.treat_msg
-
- # Found a port for the connection
- self.port = self.foundPort()
-
- if self.port is None:
- self._logger.critical("No more available slot for DCC connection")
- self.setError("Il n'y a plus de place disponible sur le serveur"
- " pour initialiser une session DCC.")
-
- def foundPort(self):
- """Found a free port for the connection"""
- for p in range(65432, 65535):
- if p not in PORTS:
- PORTS.append(p)
- return p
- return None
-
- @property
- def id(self):
- """Gives the server identifiant"""
- return self.srv.id + "/" + self.sender
-
- def setError(self, msg):
- self.error = True
- self.srv.send_msg_usr(self.sender, msg)
-
- def accept_user(self, host, port):
- """Accept a DCC connection"""
- self.s = socket.socket()
- try:
- self.s.connect((host, port))
- self._logger.info("Accepted user from %s:%d for %s", host, port, self.sender)
- self.connected = True
- self.stop = False
- except:
- self.connected = False
- self.error = True
- return False
- self.start()
- return True
-
-
- def request_user(self, type="CHAT", filename="CHAT", size=""):
- """Create a DCC connection"""
- #Open the port
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- try:
- s.bind(('', self.port))
- except:
- try:
- self.port = self.foundPort()
- s.bind(('', self.port))
- except:
- self.setError("Une erreur s'est produite durant la tentative"
- " d'ouverture d'une session DCC.")
- return False
- self._logger.info("Listening on %d for %s", self.port, self.sender)
-
- #Send CTCP request for DCC
- self.srv.send_ctcp(self.sender,
- "DCC %s %s %d %d %s" % (type, filename, self.srv.ip,
- self.port, size),
- "PRIVMSG")
-
- s.listen(1)
- #Waiting for the client
- (self.s, addr) = s.accept()
- self._logger.info("Connected by %d", addr)
- self.connected = True
- return True
-
- def send_dcc_raw(self, line):
- self.s.sendall(line + b'\n')
-
- def send_dcc(self, msg, to = None):
- """If we talk to this user, send a message through this connection
- else, send the message to the server class"""
- if to is None or to == self.sender or to == self.nick:
- if self.error:
- self.srv.send_msg_final(self.nick, msg)
- elif not self.connected or self.s is None:
- try:
- self.start()
- except RuntimeError:
- pass
- self.messages.append(msg)
- else:
- for line in msg.split("\n"):
- self.send_dcc_raw(line.encode())
- else:
- self.srv.send_dcc(msg, to)
-
- def send_file(self, filename):
- """Send a file over DCC"""
- if os.path.isfile(filename):
- self.messages = filename
- try:
- self.start()
- except RuntimeError:
- pass
- else:
- self._logger.error("File not found `%s'", filename)
-
- def run(self):
- self.stopping.clear()
-
- # Send file connection
- if not isinstance(self.messages, list):
- self.request_user("SEND",
- os.path.basename(self.messages),
- os.path.getsize(self.messages))
- if self.connected:
- with open(self.messages, 'rb') as f:
- d = f.read(268435456) #Packets size: 256Mo
- while d:
- self.s.sendall(d)
- self.s.recv(4) #The client send a confirmation after each packet
- d = f.read(268435456) #Packets size: 256Mo
-
- # Messages connection
- else:
- if not self.connected:
- if not self.request_user():
- #TODO: do something here
- return False
-
- #Start by sending all queued messages
- for mess in self.messages:
- self.send_dcc(mess)
-
- time.sleep(1)
-
- readbuffer = b''
- self.nicksize = len(self.srv.nick)
- self.Bnick = self.srv.nick.encode()
- while not self.stop:
- raw = self.s.recv(1024) #recieve server messages
- if not raw:
- break
- readbuffer = readbuffer + raw
- temp = readbuffer.split(b'\n')
- readbuffer = temp.pop()
-
- for line in temp:
- self.treatement(line)
-
- if self.connected:
- self.s.close()
- self.connected = False
-
- #Remove from DCC connections server list
- if self.realname in self.srv.dcc_clients:
- del self.srv.dcc_clients[self.realname]
-
- self._logger.info("Closing connection with %s", self.nick)
- self.stopping.set()
- if self.closing_event is not None:
- self.closing_event()
- #Rearm Thread
- threading.Thread.__init__(self)
-
- def treat_msg(self, line):
- """Treat a receive message, *can be overwritten*"""
- if line == b'NEMUBOT###':
- bot = self.srv.add_networkbot(self.srv, self.sender, self)
- self.treatement = bot.treat_msg
- self.send_dcc("NEMUBOT###")
- elif (line[:self.nicksize] == self.Bnick and
- line[self.nicksize+1:].strip()[:10] == b'my name is'):
- name = line[self.nicksize+1:].strip()[11:].decode('utf-8',
- 'replace')
- if re.match("^[a-zA-Z0-9_-]+$", name):
- if name not in self.srv.dcc_clients:
- del self.srv.dcc_clients[self.sender]
- self.nick = name
- self.sender = self.nick + "!" + self.realname
- self.srv.dcc_clients[self.realname] = self
- self.send_dcc("Hi " + self.nick)
- else:
- self.send_dcc("This nickname is already in use"
- ", please choose another one.")
- else:
- self.send_dcc("The name you entered contain"
- " invalid char.")
- else:
- self.srv.treat_msg(
- (":%s PRIVMSG %s :" % (
- self.sender,self.srv.nick)).encode() + line,
- True)
diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py
deleted file mode 100644
index 2096a63..0000000
--- a/nemubot/server/IRC.py
+++ /dev/null
@@ -1,276 +0,0 @@
-# Nemubot is a smart and modulable IM bot.
-# Copyright (C) 2012-2015 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 re
-import socket
-
-from nemubot.channel import Channel
-from nemubot.message.printer.IRC import IRC as IRCPrinter
-from nemubot.server.message.IRC import IRC as IRCMessage
-from nemubot.server.socket import SocketServer
-
-
-class IRC(SocketServer):
-
- """Concrete implementation of a connexion to an IRC server"""
-
- def __init__(self, host="localhost", port=6667, owner=None,
- nick="nemubot", username=None, password=None,
- realname="Nemubot", encoding="utf-8", caps=None,
- channels=list(), on_connect=None, **kwargs):
- """Prepare a connection with an IRC server
-
- Keyword arguments:
- host -- host to join
- port -- port on the host to reach
- ssl -- is this server using a TLS socket
- owner -- bot's owner
- nick -- bot's nick
- username -- the username as sent to server
- password -- if a password is required to connect to the server
- realname -- the bot's realname
- encoding -- the encoding used on the whole server
- caps -- client capabilities to register on the server
- channels -- list of channels to join on connection
- on_connect -- generator to call when connection is done
- """
-
- self.username = username if username is not None else nick
- self.password = password
- self.nick = nick
- self.owner = owner
- self.realname = realname
-
- super().__init__(name=self.username + "@" + host + ":" + str(port),
- host=host, port=port, **kwargs)
- self.printer = IRCPrinter
-
- self.encoding = encoding
-
- # Keep a list of joined channels
- self.channels = dict()
-
- # Server/client capabilities
- self.capabilities = caps
-
- # Register CTCP capabilities
- self.ctcp_capabilities = dict()
-
- def _ctcp_clientinfo(msg, cmds):
- """Response to CLIENTINFO CTCP message"""
- return " ".join(self.ctcp_capabilities.keys())
-
- def _ctcp_dcc(msg, cmds):
- """Response to DCC CTCP message"""
- try:
- import ipaddress
- ip = ipaddress.ip_address(int(cmds[3]))
- port = int(cmds[4])
- conn = DCC(srv, msg.sender)
- except:
- 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)
-
- if conn.accept_user(ip, port):
- srv.dcc_clients[conn.sender] = conn
- conn.send_dcc("Hello %s!" % conn.nick)
- else:
- self._logger.error("DCC: unable to connect to %s:%d", ip, port)
- return "ERRMSG unable to connect to %s:%d" % (ip, port)
-
- import nemubot
-
- self.ctcp_capabilities["ACTION"] = lambda msg, cmds: None
- self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo
- #self.ctcp_capabilities["DCC"] = _ctcp_dcc
- self.ctcp_capabilities["FINGER"] = lambda msg, cmds: "VERSION nemubot v%s" % nemubot.__version__
- self.ctcp_capabilities["NEMUBOT"] = lambda msg, cmds: "NEMUBOT %s" % nemubot.__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" % nemubot.__version__
-
- # TODO: Temporary fix, waiting for hook based CTCP management
- self.ctcp_capabilities["TYPING"] = lambda msg, cmds: None
-
- self._logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities))
-
-
- # Register hooks on some IRC CMD
- self.hookscmd = dict()
-
- # Respond to PING
- def _on_ping(msg):
- self.write(b"PONG :" + msg.params[0])
- self.hookscmd["PING"] = _on_ping
-
- # Respond to 001
- def _on_connect(msg):
- # First, send user defined command
- if on_connect is not None:
- if callable(on_connect):
- toc = on_connect()
- else:
- toc = on_connect
- if toc is not None:
- for oc in toc:
- self.write(oc)
- # Then, JOIN some channels
- for chn in channels:
- if chn.password:
- self.write("JOIN %s %s" % (chn.name, chn.password))
- else:
- self.write("JOIN %s" % chn.name)
- self.hookscmd["001"] = _on_connect
-
- # Respond to ERROR
- def _on_error(msg):
- self.close()
- self.hookscmd["ERROR"] = _on_error
-
- # Respond to CAP
- def _on_cap(msg):
- if len(msg.params) != 3 or msg.params[1] != b"LS":
- return
- server_caps = msg.params[2].decode().split(" ")
- for cap in self.capabilities:
- if cap not in server_caps:
- self.capabilities.remove(cap)
- if len(self.capabilities) > 0:
- self.write("CAP REQ :" + " ".join(self.capabilities))
- self.write("CAP END")
- self.hookscmd["CAP"] = _on_cap
-
- # Respond to JOIN
- def _on_join(msg):
- if len(msg.params) == 0:
- return
-
- for chname in msg.decode(msg.params[0]).split(","):
- # Register the channel
- chan = Channel(chname)
- self.channels[chname] = chan
- self.hookscmd["JOIN"] = _on_join
- # Respond to PART
- def _on_part(msg):
- if len(msg.params) != 1 and len(msg.params) != 2:
- return
-
- for chname in msg.params[0].split(b","):
- if chname in self.channels:
- if msg.frm == self.nick:
- del self.channels[chname]
- elif msg.frm in self.channels[chname].people:
- del self.channels[chname].people[msg.frm]
- self.hookscmd["PART"] = _on_part
- # Respond to 331/RPL_NOTOPIC,332/RPL_TOPIC,TOPIC
- def _on_topic(msg):
- if len(msg.params) != 1 and len(msg.params) != 2:
- return
- if msg.params[0] in self.channels:
- if len(msg.params) == 1 or len(msg.params[1]) == 0:
- self.channels[msg.params[0]].topic = None
- else:
- self.channels[msg.params[0]].topic = msg.decode(msg.params[1])
- self.hookscmd["331"] = _on_topic
- self.hookscmd["332"] = _on_topic
- self.hookscmd["TOPIC"] = _on_topic
- # Respond to 353/RPL_NAMREPLY
- def _on_353(msg):
- if len(msg.params) == 3:
- msg.params.pop(0) # 353: like RFC 1459
- if len(msg.params) != 2:
- return
- if msg.params[0] in self.channels:
- for nk in msg.decode(msg.params[1]).split(" "):
- res = re.match("^(?P[^a-zA-Z[\]\\`_^{|}])(?P[a-zA-Z[\]\\`_^{|}][a-zA-Z0-9[\]\\`_^{|}-]*)$")
- self.channels[msg.params[0]].people[res.group("nickname")] = res.group("level")
- self.hookscmd["353"] = _on_353
-
- # Respond to INVITE
- def _on_invite(msg):
- if len(msg.params) != 2:
- return
- self.write("JOIN " + msg.decode(msg.params[1]))
- self.hookscmd["INVITE"] = _on_invite
-
- # Respond to ERR_NICKCOLLISION
- def _on_nickcollision(msg):
- self.nick += "_"
- self.write("NICK " + self.nick)
- self.hookscmd["433"] = _on_nickcollision
- self.hookscmd["436"] = _on_nickcollision
-
- # 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.frm, res))
- self.hookscmd["PRIVMSG"] = _on_ctcp
-
-
- # Open/close
-
- def connect(self):
- super().connect()
-
- if self.password is not None:
- self.write("PASS :" + self.password)
- if self.capabilities is not None:
- self.write("CAP LS")
- self.write("NICK :" + self.nick)
- self.write("USER %s %s bla :%s" % (self.username, socket.getfqdn(), self.realname))
-
-
- def close(self):
- if not self._fd._closed:
- self.write("QUIT")
- return super().close()
-
-
- # Writes: as inherited
-
- # Read
-
- def async_read(self):
- for line in super().async_read():
- # PING should be handled here, so start parsing here :/
- msg = IRCMessage(line, self.encoding)
-
- if msg.cmd in self.hookscmd:
- self.hookscmd[msg.cmd](msg)
-
- yield msg
-
-
- def parse(self, msg):
- mes = msg.to_bot_message(self)
- if mes is not None:
- yield mes
-
-
- def subparse(self, orig, cnt):
- msg = IRCMessage(("@time=%s :%s!user@host.com PRIVMSG %s :%s" % (orig.date.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), orig.frm, ",".join(orig.to), cnt)).encode(self.encoding), self.encoding)
- return msg.to_bot_message(self)
diff --git a/nemubot/server/IRCLib.py b/nemubot/server/IRCLib.py
new file mode 100644
index 0000000..cdd13cf
--- /dev/null
+++ b/nemubot/server/IRCLib.py
@@ -0,0 +1,375 @@
+# Nemubot is a smart and modulable IM bot.
+# Copyright (C) 2012-2026 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
+import threading
+
+import irc.bot
+import irc.client
+import irc.connection
+
+import nemubot.message as message
+from nemubot.server.threaded import ThreadedServer
+
+
+class _IRCBotAdapter(irc.bot.SingleServerIRCBot):
+
+ """Internal adapter that bridges the irc library event model to nemubot.
+
+ Subclasses SingleServerIRCBot to get automatic reconnection, PING/PONG,
+ and nick-collision handling for free.
+ """
+
+ def __init__(self, server_name, push_fn, channels, on_connect_cmds,
+ nick, server_list, owner=None, realname="Nemubot",
+ encoding="utf-8", **connect_params):
+ super().__init__(server_list, nick, realname, **connect_params)
+ self._nemubot_name = server_name
+ self._push = push_fn
+ self._channels_to_join = channels
+ self._on_connect_cmds = on_connect_cmds or []
+ self.owner = owner
+ self.encoding = encoding
+ self._stop_event = threading.Event()
+
+
+ # Event loop control
+
+ def start(self):
+ """Run the reactor loop until stop() is called."""
+ self._connect()
+ while not self._stop_event.is_set():
+ self.reactor.process_once(timeout=0.2)
+
+ def stop(self):
+ """Signal the loop to exit and disconnect cleanly."""
+ self._stop_event.set()
+ try:
+ self.connection.disconnect("Goodbye")
+ except Exception:
+ pass
+
+ def on_disconnect(self, connection, event):
+ """Reconnect automatically unless we are shutting down."""
+ if not self._stop_event.is_set():
+ super().on_disconnect(connection, event)
+
+
+ # Connection lifecycle
+
+ def on_welcome(self, connection, event):
+ """001 — run on_connect commands then join channels."""
+ for cmd in self._on_connect_cmds:
+ if callable(cmd):
+ for c in (cmd() or []):
+ connection.send_raw(c)
+ else:
+ connection.send_raw(cmd)
+
+ for ch in self._channels_to_join:
+ if isinstance(ch, tuple):
+ connection.join(ch[0], ch[1] if len(ch) > 1 else "")
+ elif hasattr(ch, 'name'):
+ connection.join(ch.name, getattr(ch, 'password', "") or "")
+ else:
+ connection.join(str(ch))
+
+ def on_invite(self, connection, event):
+ """Auto-join on INVITE."""
+ if event.arguments:
+ connection.join(event.arguments[0])
+
+
+ # CTCP
+
+ def on_ctcp(self, connection, event):
+ """Handle CTCP requests (irc library >= 19 dispatches all to on_ctcp)."""
+ nick = irc.client.NickMask(event.source).nick
+ ctcp_type = event.arguments[0].upper() if event.arguments else ""
+ ctcp_arg = event.arguments[1] if len(event.arguments) > 1 else ""
+ self._reply_ctcp(connection, nick, ctcp_type, ctcp_arg)
+
+ # Fallbacks for older irc library versions that dispatch per-type
+ def on_ctcpversion(self, connection, event):
+ import nemubot
+ nick = irc.client.NickMask(event.source).nick
+ connection.ctcp_reply(nick, "VERSION nemubot v%s" % nemubot.__version__)
+
+ def on_ctcpping(self, connection, event):
+ nick = irc.client.NickMask(event.source).nick
+ arg = event.arguments[0] if event.arguments else ""
+ connection.ctcp_reply(nick, "PING %s" % arg)
+
+ def _reply_ctcp(self, connection, nick, ctcp_type, ctcp_arg):
+ import nemubot
+ responses = {
+ "ACTION": None, # handled as on_action
+ "CLIENTINFO": "CLIENTINFO ACTION CLIENTINFO FINGER PING SOURCE TIME USERINFO VERSION",
+ "FINGER": "FINGER nemubot v%s" % nemubot.__version__,
+ "PING": "PING %s" % ctcp_arg,
+ "SOURCE": "SOURCE https://github.com/nemunaire/nemubot",
+ "TIME": "TIME %s" % datetime.now(),
+ "USERINFO": "USERINFO Nemubot",
+ "VERSION": "VERSION nemubot v%s" % nemubot.__version__,
+ }
+ if ctcp_type in responses and responses[ctcp_type] is not None:
+ connection.ctcp_reply(nick, responses[ctcp_type])
+
+
+ # Incoming messages
+
+ def _decode(self, text):
+ if isinstance(text, bytes):
+ try:
+ return text.decode("utf-8")
+ except UnicodeDecodeError:
+ return text.decode(self.encoding, "replace")
+ return text
+
+ def _make_message(self, connection, source, target, text):
+ """Convert raw IRC event data into a nemubot bot message."""
+ nick = irc.client.NickMask(source).nick
+ text = self._decode(text)
+ bot_nick = connection.get_nickname()
+ is_channel = irc.client.is_channel(target)
+ to = [target] if is_channel else [nick]
+ to_response = [target] if is_channel else [nick]
+
+ common = dict(
+ server=self._nemubot_name,
+ to=to,
+ to_response=to_response,
+ frm=nick,
+ frm_owner=(nick == self.owner),
+ )
+
+ # "botname: text" or "botname, text"
+ if (text.startswith(bot_nick + ":") or
+ text.startswith(bot_nick + ",")):
+ inner = text[len(bot_nick) + 1:].strip()
+ return message.DirectAsk(designated=bot_nick, message=inner,
+ **common)
+
+ # "!command [args]"
+ if len(text) > 1 and text[0] == '!':
+ inner = text[1:].strip()
+ try:
+ args = shlex.split(inner)
+ except ValueError:
+ args = inner.split()
+ if args:
+ # Extract @key=value named arguments (same logic as IRC.py)
+ kwargs = {}
+ while len(args) > 1:
+ arg = args[1]
+ if len(arg) > 2 and arg[0:2] == '\\@':
+ args[1] = arg[1:]
+ elif len(arg) > 1 and arg[0] == '@':
+ arsp = arg[1:].split("=", 1)
+ kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None
+ args.pop(1)
+ continue
+ break
+ return message.Command(cmd=args[0], args=args[1:],
+ kwargs=kwargs, **common)
+
+ return message.Text(message=text, **common)
+
+ def on_pubmsg(self, connection, event):
+ msg = self._make_message(
+ connection, event.source, event.target,
+ event.arguments[0] if event.arguments else "",
+ )
+ if msg:
+ self._push(msg)
+
+ def on_privmsg(self, connection, event):
+ nick = irc.client.NickMask(event.source).nick
+ msg = self._make_message(
+ connection, event.source, nick,
+ event.arguments[0] if event.arguments else "",
+ )
+ if msg:
+ self._push(msg)
+
+ def on_action(self, connection, event):
+ """CTCP ACTION (/me) — delivered as a plain Text message."""
+ nick = irc.client.NickMask(event.source).nick
+ text = "/me %s" % (event.arguments[0] if event.arguments else "")
+ is_channel = irc.client.is_channel(event.target)
+ to = [event.target] if is_channel else [nick]
+ self._push(message.Text(
+ message=text,
+ server=self._nemubot_name,
+ to=to, to_response=to,
+ frm=nick, frm_owner=(nick == self.owner),
+ ))
+
+
+class IRCLib(ThreadedServer):
+
+ """IRC server using the irc Python library (jaraco).
+
+ Compared to the hand-rolled IRC.py implementation, this gets:
+ - Automatic exponential-backoff reconnection
+ - PING/PONG handled transparently
+ - Nick-collision suffix logic built-in
+ """
+
+ def __init__(self, host="localhost", port=6667, nick="nemubot",
+ username=None, password=None, realname="Nemubot",
+ encoding="utf-8", owner=None, channels=None,
+ on_connect=None, ssl=False, **kwargs):
+ """Prepare a connection to an IRC server.
+
+ Keyword arguments:
+ host -- IRC server hostname
+ port -- IRC server port (default 6667)
+ nick -- bot's nickname
+ username -- username for USER command (defaults to nick)
+ password -- server password (sent as PASS)
+ realname -- bot's real name
+ encoding -- fallback encoding for non-UTF-8 servers
+ owner -- nick of the bot's owner (sets frm_owner on messages)
+ channels -- list of channel names / (name, key) tuples to join
+ on_connect -- list of raw IRC commands (or a callable returning one)
+ to send after receiving 001
+ ssl -- wrap the connection in TLS
+ """
+ name = (username or nick) + "@" + host + ":" + str(port)
+ super().__init__(name=name)
+
+ self._host = host
+ self._port = int(port)
+ self._nick = nick
+ self._username = username or nick
+ self._password = password
+ self._realname = realname
+ self._encoding = encoding
+ self.owner = owner
+ self._channels = channels or []
+ self._on_connect_cmds = on_connect
+ self._ssl = ssl
+
+ self._bot = None
+ self._thread = None
+
+
+ # ThreadedServer hooks
+
+ def _start(self):
+ server_list = [irc.bot.ServerSpec(self._host, self._port,
+ self._password)]
+
+ connect_params = {"username": self._username}
+
+ if self._ssl:
+ import ssl as ssl_mod
+ ctx = ssl_mod.create_default_context()
+ host = self._host # capture for closure
+ connect_params["connect_factory"] = irc.connection.Factory(
+ wrapper=lambda sock: ctx.wrap_socket(sock,
+ server_hostname=host)
+ )
+
+ self._bot = _IRCBotAdapter(
+ server_name=self.name,
+ push_fn=self._push_message,
+ channels=self._channels,
+ on_connect_cmds=self._on_connect_cmds,
+ nick=self._nick,
+ server_list=server_list,
+ owner=self.owner,
+ realname=self._realname,
+ encoding=self._encoding,
+ **connect_params,
+ )
+ self._thread = threading.Thread(
+ target=self._bot.start,
+ daemon=True,
+ name="nemubot.IRC/" + self.name,
+ )
+ self._thread.start()
+
+ def _stop(self):
+ if self._bot:
+ self._bot.stop()
+ if self._thread:
+ self._thread.join(timeout=5)
+
+
+ # Outgoing messages
+
+ def send_response(self, response):
+ if response is None:
+ return
+ if isinstance(response, list):
+ for r in response:
+ self.send_response(r)
+ return
+ if not self._bot:
+ return
+
+ from nemubot.message.printer.IRCLib import IRCLib as IRCLibPrinter
+ printer = IRCLibPrinter(self._bot.connection)
+ response.accept(printer)
+
+
+ # subparse: re-parse a plain string in the context of an existing message
+ # (used by alias, rnd, grep, cat, smmry, sms modules)
+
+ def subparse(self, orig, cnt):
+ bot_nick = (self._bot.connection.get_nickname()
+ if self._bot else self._nick)
+ common = dict(
+ server=self.name,
+ to=orig.to,
+ to_response=orig.to_response,
+ frm=orig.frm,
+ frm_owner=orig.frm_owner,
+ date=orig.date,
+ )
+ text = cnt
+
+ if (text.startswith(bot_nick + ":") or
+ text.startswith(bot_nick + ",")):
+ inner = text[len(bot_nick) + 1:].strip()
+ return message.DirectAsk(designated=bot_nick, message=inner,
+ **common)
+
+ if len(text) > 1 and text[0] == '!':
+ inner = text[1:].strip()
+ try:
+ args = shlex.split(inner)
+ except ValueError:
+ args = inner.split()
+ if args:
+ kwargs = {}
+ while len(args) > 1:
+ arg = args[1]
+ if len(arg) > 2 and arg[0:2] == '\\@':
+ args[1] = arg[1:]
+ elif len(arg) > 1 and arg[0] == '@':
+ arsp = arg[1:].split("=", 1)
+ kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None
+ args.pop(1)
+ continue
+ break
+ return message.Command(cmd=args[0], args=args[1:],
+ kwargs=kwargs, **common)
+
+ return message.Text(message=text, **common)
diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py
index 9e186ed..db9ad87 100644
--- a/nemubot/server/__init__.py
+++ b/nemubot/server/__init__.py
@@ -24,13 +24,13 @@ def factory(uri, ssl=False, **init_args):
if o.scheme == "irc" or o.scheme == "ircs":
# https://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt
# https://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html
- args = init_args
+ args = dict(init_args)
if o.scheme == "ircs": ssl = True
if o.hostname is not None: args["host"] = o.hostname
if o.port is not None: args["port"] = o.port
if o.username is not None: args["username"] = o.username
- if o.password is not None: args["password"] = o.password
+ if o.password is not None: args["password"] = unquote(o.password)
modifiers = o.path.split(",")
target = unquote(modifiers.pop(0)[1:])
@@ -41,37 +41,27 @@ def factory(uri, ssl=False, **init_args):
if "msg" in params:
if "on_connect" not in args:
args["on_connect"] = []
- args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"]))
+ args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"][0]))
if "key" in params:
if "channels" not in args:
args["channels"] = []
- args["channels"].append((target, params["key"]))
+ args["channels"].append((target, params["key"][0]))
if "pass" in params:
- args["password"] = params["pass"]
+ args["password"] = params["pass"][0]
if "charset" in params:
- args["encoding"] = params["charset"]
+ args["encoding"] = params["charset"][0]
- #
if "channels" not in args and "isnick" not in modifiers:
- args["channels"] = [ target ]
+ args["channels"] = [target]
- from nemubot.server.IRC import IRC as IRCServer
+ args["ssl"] = ssl
+
+ from nemubot.server.IRCLib import IRCLib as IRCServer
srv = IRCServer(**args)
- if ssl:
- try:
- from ssl import create_default_context
- context = create_default_context()
- except ImportError:
- # Python 3.3 compat
- from ssl import SSLContext, PROTOCOL_TLSv1
- context = SSLContext(PROTOCOL_TLSv1)
- from ssl import wrap_socket
- srv._fd = context.wrap_socket(srv._fd, server_hostname=o.hostname)
-
elif o.scheme == "matrix":
# matrix://localpart:password@homeserver.tld/!room:homeserver.tld
# matrix://localpart:password@homeserver.tld/%23alias:homeserver.tld
diff --git a/nemubot/server/factory_test.py b/nemubot/server/factory_test.py
deleted file mode 100644
index efc3130..0000000
--- a/nemubot/server/factory_test.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Nemubot is a smart and modulable IM bot.
-# Copyright (C) 2012-2015 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 .
-
-import unittest
-
-from nemubot.server import factory
-
-class TestFactory(unittest.TestCase):
-
- def test_IRC1(self):
- from nemubot.server.IRC import IRC as IRCServer
- import socket
- import ssl
-
- # : If omitted, the client must connect to a prespecified default IRC server.
- server = factory("irc:///")
- self.assertIsInstance(server, IRCServer)
- self.assertIsInstance(server._fd, socket.socket)
- self.assertIn(server._sockaddr[0], ["127.0.0.1", "::1"])
-
- server = factory("irc://2.2.2.2")
- self.assertIsInstance(server, IRCServer)
- self.assertEqual(server._sockaddr[0], "2.2.2.2")
-
- server = factory("ircs://1.2.1.2")
- self.assertIsInstance(server, IRCServer)
- self.assertIsInstance(server._fd, ssl.SSLSocket)
-
- server = factory("irc://1.2.3.4:6667")
- self.assertIsInstance(server, IRCServer)
- self.assertEqual(server._sockaddr[0], "1.2.3.4")
- self.assertEqual(server._sockaddr[1], 6667)
-
- server = factory("ircs://4.3.2.1:194/")
- self.assertIsInstance(server, IRCServer)
- self.assertEqual(server._sockaddr[0], "4.3.2.1")
- self.assertEqual(server._sockaddr[1], 194)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py
deleted file mode 100644
index 5ccd735..0000000
--- a/nemubot/server/message/IRC.py
+++ /dev/null
@@ -1,210 +0,0 @@
-# Nemubot is a smart and modulable IM bot.
-# Copyright (C) 2012-2016 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, timezone
-import re
-import shlex
-
-import nemubot.message as message
-from nemubot.server.message.abstract import Abstract
-
-mgx = re.compile(b'''^(?:@(?P[^ ]+)\ )?
- (?::(?P
- (?P[^!@ ]+)
- (?: !(?P[^@ ]+))?
- (?:@(?P[^ ]*))?
- )\ )?
- (?P(?:[a-zA-Z]+|[0-9]{3}))
- (?P(?:\ [^:][^ ]*)*)(?:\ :(?P.*))?
- $''', re.X)
-
-class IRC(Abstract):
-
- """Class responsible for parsing IRC messages"""
-
- def __init__(self, raw, encoding="utf-8"):
- self.encoding = encoding
- self.tags = { 'time': datetime.now(timezone.utc) }
- self.params = list()
-
- p = mgx.match(raw.rstrip())
-
- if p is None:
- raise Exception("Not a valid IRC message: %s" % raw)
-
- # Parse tags if exists: @aaa=bbb;ccc;example.com/ddd=eee
- if p.group("tags"):
- for tgs in self.decode(p.group("tags")).split(';'):
- tag = tgs.split('=')
- if len(tag) > 1:
- self.add_tag(tag[0], tag[1])
- else:
- self.add_tag(tag[0])
-
- # Parse prefix if exists: :nick!user@host.com
- self.prefix = self.decode(p.group("prefix"))
- self.nick = self.decode(p.group("nick"))
- self.user = self.decode(p.group("user"))
- self.host = self.decode(p.group("host"))
-
- # Parse command
- self.cmd = self.decode(p.group("command"))
-
- # Parse params
- if p.group("params") is not None and p.group("params") != b'':
- for param in p.group("params").strip().split(b' '):
- self.params.append(param)
-
- if p.group("trailing") is not None:
- self.params.append(p.group("trailing"))
-
-
- def add_tag(self, key, value=None):
- """Add an IRCv3.2 Message Tags
-
- Arguments:
- key -- tag identifier (unique for the message)
- value -- optional value for the tag
- """
-
- # Treat special tags
- if key == "time" and value is not None:
- import calendar, time
- value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc)
-
- # Store tag
- self.tags[key] = value
-
-
- @property
- def is_ctcp(self):
- """Analyze a message, to determine if this is a CTCP one"""
- 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
-
- Argument:
- s -- string to decode
- """
-
- if isinstance(s, bytes):
- try:
- s = s.decode()
- except UnicodeDecodeError:
- s = s.decode(self.encoding, 'replace')
- return s
-
-
-
- def to_server_string(self, client=True):
- """Pretty print the message to close to original input string
-
- Keyword argument:
- client -- export as a client-side string if true
- """
-
- res = ";".join(["@%s=%s" % (k, v if not isinstance(v, datetime) else v.strftime("%Y-%m-%dT%H:%M:%S.%fZ")) for k, v in self.tags.items()])
-
- if not client:
- res += " :%s!%s@%s" % (self.nick, self.user, self.host)
-
- res += " " + self.cmd
-
- if len(self.params) > 0:
-
- if len(self.params) > 1:
- res += " " + self.decode(b" ".join(self.params[:-1]))
- res += " :" + self.decode(self.params[-1])
-
- return res
-
-
- def to_bot_message(self, srv):
- """Convert to one of concrete implementation of AbstractMessage
-
- Argument:
- srv -- the server from the message was received
- """
-
- if self.cmd == "PRIVMSG" or self.cmd == "NOTICE":
-
- receivers = self.decode(self.params[0]).split(',')
-
- common_args = {
- "server": srv.name,
- "date": self.tags["time"],
- "to": receivers,
- "to_response": [r if r != srv.nick else self.nick for r in receivers],
- "frm": self.nick,
- "frm_owner": self.nick == srv.owner
- }
-
- # 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 text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":":
- designated = srv.nick
- text = text[len(srv.nick) + 1:].strip()
- else:
- designated = None
-
- # Is this a command?
- 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(' ')
-
- # Extract explicit named arguments: @key=value or just @key, only at begening
- kwargs = {}
- while len(args) > 1:
- arg = args[1]
- if len(arg) > 2:
- if arg[0:2] == '\\@':
- args[1] = arg[1:]
- elif arg[0] == '@':
- arsp = arg[1:].split("=", 1)
- if len(arsp) == 2:
- kwargs[arsp[0]] = arsp[1]
- else:
- kwargs[arg[1:]] = None
- args.pop(1)
- continue
- # Futher argument are considered as normal argument (this helps for subcommand treatment)
- break
-
- return message.Command(cmd=args[0],
- args=args[1:],
- kwargs=kwargs,
- **common_args)
-
- # Is this an ask for this bot?
- elif designated is not None:
- return message.DirectAsk(designated=designated, message=text, **common_args)
-
- # Normal message
- else:
- return message.Text(message=text, **common_args)
-
- return None
diff --git a/nemubot/server/message/__init__.py b/nemubot/server/message/__init__.py
deleted file mode 100644
index 57f3468..0000000
--- a/nemubot/server/message/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Nemubot is a smart and modulable IM bot.
-# Copyright (C) 2012-2015 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 .
diff --git a/nemubot/server/message/abstract.py b/nemubot/server/message/abstract.py
deleted file mode 100644
index 624e453..0000000
--- a/nemubot/server/message/abstract.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Nemubot is a smart and modulable IM bot.
-# Copyright (C) 2012-2016 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 .
-
-class Abstract:
-
- def to_bot_message(self, srv):
- """Convert to one of concrete implementation of AbstractMessage
-
- Argument:
- srv -- the server from the message was received
- """
-
- raise NotImplemented
-
-
- def to_server_string(self, **kwargs):
- """Pretty print the message to close to original input string
- """
-
- raise NotImplemented
diff --git a/nemubot/server/test_IRC.py b/nemubot/server/test_IRC.py
deleted file mode 100644
index 552a1d3..0000000
--- a/nemubot/server/test_IRC.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import unittest
-
-import nemubot.server.IRC as IRC
-
-
-class TestIRCMessage(unittest.TestCase):
-
-
- def setUp(self):
- self.msg = IRC.IRCMessage(b":toto!titi@RZ-3je16g.re PRIVMSG #the-channel :Can you parse this message?")
-
-
- def test_parsing(self):
- self.assertEqual(self.msg.prefix, "toto!titi@RZ-3je16g.re")
- self.assertEqual(self.msg.nick, "toto")
- self.assertEqual(self.msg.user, "titi")
- self.assertEqual(self.msg.host, "RZ-3je16g.re")
-
- self.assertEqual(len(self.msg.params), 2)
-
- self.assertEqual(self.msg.params[0], b"#the-channel")
- self.assertEqual(self.msg.params[1], b"Can you parse this message?")
-
-
- def test_prettyprint(self):
- bst1 = self.msg.to_server_string(False)
- msg2 = IRC.IRCMessage(bst1.encode())
-
- bst2 = msg2.to_server_string(False)
- msg3 = IRC.IRCMessage(bst2.encode())
-
- bst3 = msg3.to_server_string(False)
-
- self.assertEqual(bst2, bst3)
-
-
- def test_tags(self):
- self.assertEqual(len(self.msg.tags), 1)
- self.assertIn("time", self.msg.tags)
-
- self.msg.add_tag("time")
- self.assertEqual(len(self.msg.tags), 1)
-
- self.msg.add_tag("toto")
- self.assertEqual(len(self.msg.tags), 2)
- self.assertIn("toto", self.msg.tags)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/requirements.txt b/requirements.txt
index 45eefe2..e037895 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
+irc
matrix-nio
diff --git a/setup.py b/setup.py
index 94c1274..7b5bdcd 100755
--- a/setup.py
+++ b/setup.py
@@ -71,7 +71,6 @@ setup(
'nemubot.message.printer',
'nemubot.module',
'nemubot.server',
- 'nemubot.server.message',
'nemubot.tools',
'nemubot.tools.xmlparser',
],