diff --git a/Dockerfile b/Dockerfile index a027b9e..b830622 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ FROM python:3.11-alpine WORKDIR /usr/src/app COPY requirements.txt /usr/src/app/ -RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr && \ - pip install --no-cache-dir -r requirements.txt && \ +RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr py3-matrix-nio && \ + pip install --no-cache-dir --ignore-installed -r requirements.txt && \ pip install bs4 capstone dnspython openai && \ apk del build-base capstone-dev && \ ln -s /var/lib/nemubot/home /home/nemubot 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/message/printer/Matrix.py b/nemubot/message/printer/Matrix.py new file mode 100644 index 0000000..ad1b99e --- /dev/null +++ b/nemubot/message/printer/Matrix.py @@ -0,0 +1,69 @@ +# 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 new file mode 100644 index 0000000..3051028 --- /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 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 new file mode 100644 index 0000000..ed4b746 --- /dev/null +++ b/nemubot/server/Matrix.py @@ -0,0 +1,200 @@ +# 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 a39c491..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,36 +41,58 @@ 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 + # Use matrixs:// for https (default) vs http + args = dict(init_args) + 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/requirements.txt b/requirements.txt index e69de29..e037895 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,2 @@ +irc +matrix-nio