diff --git a/matrixbot/commands.py b/matrixbot/commands.py
index 3ada0ee..3db7bed 100644
--- a/matrixbot/commands.py
+++ b/matrixbot/commands.py
@@ -13,7 +13,7 @@ import aiohttp
from nio import AsyncClient
-from utils import send_text, send_html, send_reaction, edit_html, sanitize_input
+from utils import send_text, send_html, send_reaction, edit_html, sanitize_input, rcon_command, RconError
from wordle import handle_wordle, wordle_stats as _wordle_stats
from welcome import clean_stale_dm_messages
from config import (
@@ -1317,29 +1317,40 @@ async def cmd_minecraft(client: AsyncClient, room_id: str, sender: str, args: st
await send_text(client, room_id, f"Whitelisting {username}...")
try:
- from mcrcon import MCRcon
-
def _rcon():
- with MCRcon(MINECRAFT_RCON_HOST, MINECRAFT_RCON_PASSWORD, port=MINECRAFT_RCON_PORT, timeout=3) as mcr:
- return mcr.command(f"whitelist add {username}")
+ return rcon_command(
+ MINECRAFT_RCON_HOST,
+ MINECRAFT_RCON_PASSWORD,
+ f"whitelist add {username}",
+ port=MINECRAFT_RCON_PORT,
+ timeout=RCON_TIMEOUT,
+ )
loop = asyncio.get_running_loop()
response = await asyncio.wait_for(loop.run_in_executor(None, _rcon), timeout=RCON_TIMEOUT)
logger.info(f"RCON response: {response}")
- plain = f"Minecraft\nYou have been whitelisted on the SMP!\nServer: minecraft.lotusguild.org\nUsername: {username}"
+ already = "already whitelisted" in response.lower()
+ status_line = (
+ f"↺ {username} was already whitelisted."
+ if already
+ else f"✅ {username} is whitelisted and ready to join."
+ )
+ plain = (
+ f"✿ Minecraft — Lotus SMP ✿\n"
+ f"{'Already whitelisted' if already else 'Whitelisted'}: {username}\n"
+ f"Server: minecraft.lotusguild.org"
+ )
html = (
- f"Minecraft
"
- f"You have been whitelisted on the SMP!
"
- f"Server: minecraft.lotusguild.org
"
- f"Username: {username}"
+ f'✿ Minecraft — Lotus SMP ✿
'
+ f"
" + f"{status_line}" ) await send_html(client, room_id, plain, html) - except ImportError: - await send_text(client, room_id, "mcrcon is not installed. Ask an admin to install it.") - except asyncio.TimeoutError: - await send_text(client, room_id, "Minecraft server timed out. It may be offline.") - except Exception as e: + except (asyncio.TimeoutError, RconError, OSError) as e: logger.error(f"RCON error: {e}", exc_info=True) await send_text(client, room_id, "Failed to whitelist. The server may be offline (let jared know).") diff --git a/matrixbot/requirements.txt b/matrixbot/requirements.txt index 1d79d03..a4f80f9 100644 --- a/matrixbot/requirements.txt +++ b/matrixbot/requirements.txt @@ -2,4 +2,3 @@ matrix-nio[e2e] python-dotenv>=1.2.2 aiohttp markdown -mcrcon diff --git a/matrixbot/utils.py b/matrixbot/utils.py index cb047cb..a7040a2 100644 --- a/matrixbot/utils.py +++ b/matrixbot/utils.py @@ -155,3 +155,50 @@ def sanitize_input(text: str, max_length: int = MAX_INPUT_LENGTH) -> str: text = text.strip()[:max_length] text = "".join(char for char in text if char.isprintable()) return text + + +class RconError(Exception): + pass + + +def rcon_command(host: str, password: str, command: str, port: int = 25575, timeout: float = 5.0) -> str: + """Minimal Source RCON client (Minecraft protocol) using socket timeouts. + + Deliberately avoids the `mcrcon` package: it implements its read timeout + via signal.SIGALRM, which only works on the main thread — this is always + called from a worker thread (via loop.run_in_executor), so that library + raises "signal only works in main thread of the main interpreter" every + time. socket.settimeout() has no such restriction. + """ + import socket + import struct + + def send_packet(sock, pkt_id, pkt_type, payload): + body = struct.pack("
" + f'Server:minecraft.lotusguild.org
' + f"fresh world · hard mode · no takebacks" + f"