from subprocess import Popen, PIPE from typing import Union import asyncio import json import sys import re from nbsr import NonBlockingStreamReader as NBSR import config # run the shell as a subprocess: p = Popen(sys.argv[1:], stdin = PIPE, stdout = PIPE, stderr = PIPE, shell = False) # wrap p.stdout with a NonBlockingStreamReader object: nbsr = NBSR(p.stdout) async def start(client, mc_channel): """ Start reading from the Minecraft subprocess. """ await asyncio.sleep(3) while True: output = nbsr.readline(0.1) # 0.1 secs to let the shell output the result if not output: await asyncio.sleep(1) else: try: sentence = output.decode("utf-8").strip() except UnicodeDecodeError: print("Could not decode sentence:") print(output) else: print(sentence) plain_text, sentence = process_message(sentence) if sentence is not None: print("[Matrix] " + plain_text) # Send terminal message to Matrix await client.room_send( room_id=mc_channel, message_type="m.room.message", content = { "msgtype": "m.text", "body": plain_text, "format": "org.matrix.custom.html", "formatted_body": sentence } ) server_live = False def process_message(sentence : str) -> Union[str, None]: """ Process a message that is sent to stdout in the Minecraft terminal. If this function deems it relevant, it returns a string that can be sent to Matrix. """ global server_live if (match := re.fullmatch( r"\[[\d:]+\] \[Server thread\/INFO\]: Preparing level \"(.+)\"" , sentence)): level, = match.groups() return f"Preparing level {level}...", "Preparing level \"" + level + "\"..." if re.fullmatch( r"\[[\d:]+\] \[Server thread\/INFO\]: Done \(\d+.?\d*s\)! For help, type \"help\"", sentence): server_live = True return f"The Minecraft server is live. The server is reacable at {config.SERVER_IP}.", f"The minecraft server is live. The server is reacable at {config.SERVER_IP}." if re.fullmatch( r"\[[\d:]+\] \[Server thread\/INFO\]: Stopping server", sentence): return "The server has stopped.", "The server has stopped." if not server_live: return None, None if (match := re.fullmatch( r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) joined the game", sentence)): username, = match.groups() return username + " joined the Minecraft server", ( "" + username + " joined the Minecraft server") if (match := re.fullmatch( r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) left the game", sentence)): username, = match.groups() return username + " left the Minecraft server", ( "" + username + " left the Minecraft server" ) if (match := re.fullmatch( r"\[[\d:]+\] \[Server thread\/INFO\]: <([A-Za-z0-9_]{3,16})> (.+)", sentence)): username, message = match.groups() return username + ": " + message, ( "" + username + ": " + message ) if (match := re.fullmatch( r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) ([\w\[\]\-\. !?,]+)", sentence)): message = " ".join(match.groups()) return message, "" + message + "" return None, None def reply_to_mc(message : str, display_name : str, sender : str): """ Send something back to the Minecraft terminal. """ if sender in config.MATRIX_ADMINS and message.startswith('!'): p.stdin.write((message[1:] + "\r\n").encode()) else: p.stdin.write( ("execute as @a run tellraw @s " + format(sender, display_name, message) + "\r\n").encode() ) p.stdin.flush() def format(sender : str, display_name : str, message : str) -> str: """ Create a string used to format the user's message. """ start = [ "", dict(text="M", color="red" ) ] end = [ dict(text=f" <{display_name}> {message}") ] options = config.at(['matrix', 'alternative_platforms']) or {} for platform, details in options.items(): try: regex = details['match'] text = details['text'] color = details['color'] except KeyError: print("WARNING: Platform `" + platform + "` is missing some configurations.") else: if re.fullmatch(regex, sender): start.append(dict(text=text, color=color)) return json.dumps(start + end) if __name__ == '__main__': asyncio.run(start())