diff --git a/nemubot/message/printer/IRCLib.py b/nemubot/message/printer/IRCLib.py
deleted file mode 100644
index abd1f2f..0000000
--- a/nemubot/message/printer/IRCLib.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# 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/message/printer/Matrix.py b/nemubot/message/printer/Matrix.py
deleted file mode 100644
index ad1b99e..0000000
--- a/nemubot/message/printer/Matrix.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# 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 Matrix(AbstractVisitor):
-
- """Visitor that sends bot responses as Matrix room messages.
-
- Instead of accumulating text like the IRC printer does, each visit_*
- method calls send_func(room_id, text) directly for every destination room.
- """
-
- def __init__(self, send_func):
- """
- Argument:
- send_func -- callable(room_id: str, text: str) that sends a plain-text
- message to the given Matrix room
- """
- self._send = send_func
-
- def visit_Text(self, msg):
- if isinstance(msg.message, str):
- for room in msg.to:
- self._send(room, msg.message)
- else:
- # Nested message object — let it visit itself
- msg.message.accept(self)
-
- def visit_DirectAsk(self, msg):
- text = msg.message if isinstance(msg.message, str) else str(msg.message)
- # Rooms that are NOT the designated nick → prefix with "nick: "
- others = [to for to in msg.to if to != msg.designated]
- if len(others) == 0 or len(others) != len(msg.to):
- # At least one room IS the designated target → send plain
- for room in msg.to:
- self._send(room, text)
- if len(others):
- # Other rooms → prefix with nick
- for room in others:
- self._send(room, "%s: %s" % (msg.designated, text))
-
- def visit_Command(self, msg):
- parts = ["!" + msg.cmd]
- if msg.args:
- parts.extend(msg.args)
- for room in msg.to:
- self._send(room, " ".join(parts))
-
- def visit_OwnerCommand(self, msg):
- parts = ["`" + msg.cmd]
- if msg.args:
- parts.extend(msg.args)
- for room in msg.to:
- self._send(room, " ".join(parts))
diff --git a/nemubot/server/IRCLib.py b/nemubot/server/IRCLib.py
deleted file mode 100644
index 3051028..0000000
--- a/nemubot/server/IRCLib.py
+++ /dev/null
@@ -1,375 +0,0 @@
-# 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 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
- import irc.connection
- 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/Matrix.py b/nemubot/server/Matrix.py
deleted file mode 100644
index ed4b746..0000000
--- a/nemubot/server/Matrix.py
+++ /dev/null
@@ -1,200 +0,0 @@
-# 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 .
-
-import asyncio
-import shlex
-import threading
-
-import nemubot.message as message
-from nemubot.server.threaded import ThreadedServer
-
-
-class Matrix(ThreadedServer):
-
- """Matrix server implementation using matrix-nio's AsyncClient.
-
- Runs an asyncio event loop in a daemon thread. Incoming room messages are
- converted to nemubot bot messages and pushed through the pipe; outgoing
- responses are sent via the async client from the same event loop.
- """
-
- def __init__(self, homeserver, user_id, password=None, access_token=None,
- owner=None, nick=None, channels=None, **kwargs):
- """Prepare a connection to a Matrix homeserver.
-
- Keyword arguments:
- homeserver -- base URL of the homeserver, e.g. "https://matrix.org"
- user_id -- full MXID (@user:server) or bare localpart
- password -- login password (required if no access_token)
- access_token -- pre-obtained access token (alternative to password)
- owner -- MXID of the bot owner (marks frm_owner on messages)
- nick -- display name / prefix for DirectAsk detection
- channels -- list of room IDs / aliases to join on connect
- """
-
- # Ensure fully-qualified MXID
- if not user_id.startswith("@"):
- host = homeserver.split("//")[-1].rstrip("/")
- user_id = "@%s:%s" % (user_id, host)
-
- super().__init__(name=user_id)
-
- self.homeserver = homeserver
- self.user_id = user_id
- self.password = password
- self.access_token = access_token
- self.owner = owner
- self.nick = nick or user_id
-
- self._initial_rooms = channels or []
- self._client = None
- self._loop = None
- self._thread = None
-
-
- # Open/close
-
- def _start(self):
- self._thread = threading.Thread(
- target=self._run_loop,
- daemon=True,
- name="nemubot.Matrix/" + self._name,
- )
- self._thread.start()
-
- def _stop(self):
- if self._client and self._loop and not self._loop.is_closed():
- try:
- asyncio.run_coroutine_threadsafe(
- self._client.close(), self._loop
- ).result(timeout=5)
- except Exception:
- self._logger.exception("Error while closing Matrix client")
- if self._thread:
- self._thread.join(timeout=5)
-
-
- # Asyncio thread
-
- def _run_loop(self):
- self._loop = asyncio.new_event_loop()
- asyncio.set_event_loop(self._loop)
- try:
- self._loop.run_until_complete(self._async_main())
- except Exception:
- self._logger.exception("Unhandled exception in Matrix event loop")
- finally:
- self._loop.close()
-
- async def _async_main(self):
- from nio import AsyncClient, LoginError, RoomMessageText
-
- self._client = AsyncClient(self.homeserver, self.user_id)
-
- if self.access_token:
- self._client.access_token = self.access_token
- self._logger.info("Using provided access token for %s", self.user_id)
- elif self.password:
- resp = await self._client.login(self.password)
- if isinstance(resp, LoginError):
- self._logger.error("Matrix login failed: %s", resp.message)
- return
- self._logger.info("Logged in to Matrix as %s", self.user_id)
- else:
- self._logger.error("Need either password or access_token to connect")
- return
-
- self._client.add_event_callback(self._on_room_message, RoomMessageText)
-
- for room in self._initial_rooms:
- await self._client.join(room)
- self._logger.info("Joined room %s", room)
-
- await self._client.sync_forever(timeout=30000, full_state=True)
-
-
- # Incoming messages
-
- async def _on_room_message(self, room, event):
- """Callback invoked by matrix-nio for each m.room.message event."""
-
- if event.sender == self.user_id:
- return # ignore own messages
-
- text = event.body
- room_id = room.room_id
- frm = event.sender
-
- common_args = {
- "server": self.name,
- "to": [room_id],
- "to_response": [room_id],
- "frm": frm,
- "frm_owner": frm == self.owner,
- }
-
- if len(text) > 1 and text[0] == '!':
- text = text[1:].strip()
- try:
- args = shlex.split(text)
- except ValueError:
- args = text.split(' ')
- msg = message.Command(cmd=args[0], args=args[1:], **common_args)
-
- elif (text.lower().startswith(self.nick.lower() + ":")
- or text.lower().startswith(self.nick.lower() + ",")):
- text = text[len(self.nick) + 1:].strip()
- msg = message.DirectAsk(designated=self.nick, message=text,
- **common_args)
-
- else:
- msg = message.Text(message=text, **common_args)
-
- self._push_message(msg)
-
-
- # 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
-
- from nemubot.message.printer.Matrix import Matrix as MatrixPrinter
- printer = MatrixPrinter(self._send_text)
- response.accept(printer)
-
- def _send_text(self, room_id, text):
- """Thread-safe: schedule a Matrix room_send on the asyncio loop."""
- if not self._client or not self._loop or self._loop.is_closed():
- self._logger.warning("Cannot send: Matrix client not ready")
- return
- future = asyncio.run_coroutine_threadsafe(
- self._client.room_send(
- room_id=room_id,
- message_type="m.room.message",
- content={"msgtype": "m.text", "body": text},
- ignore_unverified_devices=True,
- ),
- self._loop,
- )
- future.add_done_callback(
- lambda f: self._logger.warning("Matrix send error: %s", f.exception())
- if not f.cancelled() and f.exception() else None
- )
diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py
index db9ad87..a39c491 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 = dict(init_args)
+ args = 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"] = unquote(o.password)
+ if o.password is not None: args["password"] = o.password
modifiers = o.path.split(",")
target = unquote(modifiers.pop(0)[1:])
@@ -41,58 +41,36 @@ 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"][0]))
+ args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"]))
if "key" in params:
if "channels" not in args:
args["channels"] = []
- args["channels"].append((target, params["key"][0]))
+ args["channels"].append((target, params["key"]))
if "pass" in params:
- args["password"] = params["pass"][0]
+ args["password"] = params["pass"]
if "charset" in params:
- args["encoding"] = params["charset"][0]
+ args["encoding"] = params["charset"]
+ #
if "channels" not in args and "isnick" not in modifiers:
- args["channels"] = [target]
+ args["channels"] = [ target ]
- args["ssl"] = ssl
-
- from nemubot.server.IRCLib import IRCLib as IRCServer
+ from nemubot.server.IRC import IRC as IRCServer
srv = IRCServer(**args)
- elif o.scheme == "matrix":
- # matrix://localpart:password@homeserver.tld/!room:homeserver.tld
- # matrix://localpart:password@homeserver.tld/%23alias:homeserver.tld
- # Use matrixs:// for https (default) vs http
- args = dict(init_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)
- homeserver = "https://" + o.hostname
- if o.port is not None:
- homeserver += ":%d" % o.port
- args["homeserver"] = homeserver
-
- if o.username is not None:
- args["user_id"] = o.username
- if o.password is not None:
- args["password"] = unquote(o.password)
-
- # Parse rooms from path (comma-separated, URL-encoded)
- if o.path and o.path != "/":
- rooms = [unquote(r) for r in o.path.lstrip("/").split(",") if r]
- if rooms:
- args.setdefault("channels", []).extend(rooms)
-
- params = parse_qs(o.query)
- if "token" in params:
- args["access_token"] = params["token"][0]
- if "nick" in params:
- args["nick"] = params["nick"][0]
- if "owner" in params:
- args["owner"] = params["owner"][0]
-
- from nemubot.server.Matrix import Matrix as MatrixServer
- srv = MatrixServer(**args)
return srv
diff --git a/nemubot/server/threaded.py b/nemubot/server/threaded.py
deleted file mode 100644
index eb1ae19..0000000
--- a/nemubot/server/threaded.py
+++ /dev/null
@@ -1,132 +0,0 @@
-# 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 .
-
-import logging
-import os
-import queue
-
-from nemubot.bot import sync_act
-
-
-class ThreadedServer:
-
- """A server backed by a library running in its own thread.
-
- Uses an os.pipe() as a fake file descriptor to integrate with the bot's
- select.poll() main loop without requiring direct socket access.
-
- When the library thread has a message ready, it calls _push_message(),
- which writes a wakeup byte to the pipe's write end. The bot's poll loop
- sees the read end become readable, calls async_read(), which drains the
- message queue and yields already-parsed bot-level messages.
-
- This abstraction lets any IM library (IRC via python-irc, Matrix via
- matrix-nio, …) plug into nemubot without touching bot.py.
- """
-
- def __init__(self, name):
- self._name = name
- self._logger = logging.getLogger("nemubot.server." + name)
- self._queue = queue.Queue()
- self._pipe_r, self._pipe_w = os.pipe()
-
-
- @property
- def name(self):
- return self._name
-
- def fileno(self):
- return self._pipe_r
-
-
- # Open/close
-
- def connect(self):
- """Start the library and register the pipe read-end with the poll loop."""
- self._logger.info("Starting connection")
- self._start()
- sync_act("sckt", "register", self._pipe_r)
-
- def _start(self):
- """Override: start the library's connection (e.g. launch a thread)."""
- raise NotImplementedError
-
- def close(self):
- """Unregister from poll, stop the library, and close the pipe."""
- self._logger.info("Closing connection")
- sync_act("sckt", "unregister", self._pipe_r)
- self._stop()
- for fd in (self._pipe_w, self._pipe_r):
- try:
- os.close(fd)
- except OSError:
- pass
-
- def _stop(self):
- """Override: stop the library thread gracefully."""
- pass
-
-
- # Writes
-
- def send_response(self, response):
- """Override: send a response via the underlying library."""
- raise NotImplementedError
-
- def async_write(self):
- """No-op: writes go directly through the library, not via poll."""
- pass
-
-
- # Read
-
- def _push_message(self, msg):
- """Called from the library thread to enqueue a bot-level message.
-
- Writes a wakeup byte to the pipe so the main loop wakes up and
- calls async_read().
- """
- self._queue.put(msg)
- try:
- os.write(self._pipe_w, b'\x00')
- except OSError:
- pass # pipe closed during shutdown
-
- def async_read(self):
- """Called by the bot when the pipe is readable.
-
- Drains the wakeup bytes and yields all queued bot messages.
- """
- try:
- os.read(self._pipe_r, 256)
- except OSError:
- return
- while not self._queue.empty():
- try:
- yield self._queue.get_nowait()
- except queue.Empty:
- break
-
- def parse(self, msg):
- """Messages pushed via _push_message are already bot-level — pass through."""
- yield msg
-
-
- # Exceptions
-
- def exception(self, flags):
- """Called by the bot on POLLERR/POLLHUP/POLLNVAL."""
- self._logger.warning("Exception on server %s: flags=0x%x", self._name, flags)
diff --git a/requirements.txt b/requirements.txt
index f709273..e69de29 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +0,0 @@
-irc
-matrix-nio[e2e]