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())