Move Python scripts to src/ folder

main
Bram van den Heuvel 2023-10-01 16:33:10 +02:00
parent 94155b34a1
commit 5d86b17a01
4 changed files with 293 additions and 275 deletions

View File

@ -1,25 +1,25 @@
""" """
This module prepares the necessary files for running the server in the This module prepares the necessary files for running the server in the
correct configuration. correct configuration.
""" """
import config import config
def write_eula(): def write_eula():
""" """
Write whether the user accepts to Minecraft's EULA. Write whether the user accepts to Minecraft's EULA.
The server refuses to run unless explicitly accepted. The server refuses to run unless explicitly accepted.
""" """
with open("eula.txt", 'w') as fp: with open("eula.txt", 'w') as fp:
if config.EULA == True: if config.EULA == True:
fp.write("eula=true") fp.write("eula=true")
else: else:
fp.write("eula=false") fp.write("eula=false")
def write_server_properties(): def write_server_properties():
""" """
Write the configuration for the Minecraft world. Write the configuration for the Minecraft world.
""" """
with open("server.properties", 'w') as fp: with open("server.properties", 'w') as fp:
for key, value in config.at(['minecraft']).items(): for key, value in config.at(['minecraft']).items():
fp.write(f"{key}={value}\n") fp.write(f"{key}={value}\n")

View File

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

View File

@ -1,61 +1,60 @@
import asyncio import asyncio
import time import time
import re import re
from nio import AsyncClient, MatrixRoom, RoomMessageText from nio import AsyncClient, MatrixRoom, RoomMessageText
import mc_wrapper import src.mc_wrapper as mc_wrapper
import config import config
import build_server
STARTUP_TIME = time.time()
STARTUP_TIME = time.time()
client = AsyncClient(config.MATRIX_HOMESERVER, config.MATRIX_USERNAME)
client = AsyncClient(config.MATRIX_HOMESERVER, config.MATRIX_USERNAME)
async def message_callback(room: MatrixRoom, event: RoomMessageText) -> None:
async def message_callback(room: MatrixRoom, event: RoomMessageText) -> None: if room.machine_name != config.MATRIX_ROOM:
if room.machine_name != config.MATRIX_ROOM: return
return if event.sender == client.user_id:
if event.sender == client.user_id: return
return if int(event.server_timestamp) < STARTUP_TIME:
if int(event.server_timestamp) < STARTUP_TIME: return
return
# Determine how to display username
# Determine how to display username name = room.users[event.sender].display_name
name = room.users[event.sender].display_name for user in room.users:
for user in room.users: if user == event.sender:
if user == event.sender: continue
continue
if room.users[user].display_name == name:
if room.users[user].display_name == name: name = room.users[event.sender].disambiguated_name
name = room.users[event.sender].disambiguated_name break
break
mc_wrapper.reply_to_mc(event.body, name, event.sender)
mc_wrapper.reply_to_mc(event.body, name, event.sender) client.add_event_callback(message_callback, RoomMessageText)
client.add_event_callback(message_callback, RoomMessageText)
async def activate_client() -> None:
async def activate_client() -> None: print(await client.login(config.MATRIX_PASSWORD))
print(await client.login(config.MATRIX_PASSWORD))
await client.room_send(
await client.room_send( room_id=config.MATRIX_ROOM,
room_id=config.MATRIX_ROOM, message_type="m.room.message",
message_type="m.room.message", content = {
content = { "msgtype": "m.text",
"msgtype": "m.text", "body": "Starting Minecraft-Matrix bridge...",
"body": "Starting Minecraft-Matrix bridge...", "format": "org.matrix.custom.html",
"format": "org.matrix.custom.html", "formatted_body": "<strong>Starting Minecraft-Matrix bridge...</strong>"
"formatted_body": "<strong>Starting Minecraft-Matrix bridge...</strong>" }
} )
) await client.sync_forever(timeout=30000) # milliseconds
await client.sync_forever(timeout=30000) # milliseconds
async def start():
async def start(): await asyncio.gather(
await asyncio.gather( # Start the Matrix client
# Start the Matrix client activate_client(),
activate_client(),
# Start the Minecraft subprocess
# Start the Minecraft subprocess mc_wrapper.start(client, config.MATRIX_ROOM)
mc_wrapper.start(client, config.MATRIX_ROOM) )
)
asyncio.run(start()) asyncio.run(start())

View File

@ -1,39 +1,51 @@
from threading import Thread """
from queue import Queue, Empty The NBSR module defines a Non-blocking stream reader (NBSR class).
class NonBlockingStreamReader: In short, the Minecraft stdout is a stream of data that doesn't end until
the server shuts down. Traditionally, Python would not run any code until
def __init__(self, stream): the server has shut down and returns its entire output.
'''
stream: the stream to read from. The NBSR class allows us to read from the stream without blocking the entire
Usually a process' stdout or stderr. Python script. We will occasionally ask the NBSR for any updates, and it
''' will give us the latest output, if it exists.
"""
self._s = stream
self._q = Queue() from threading import Thread
from queue import Queue, Empty
def _populateQueue(stream, queue):
''' class NonBlockingStreamReader:
Collect lines from 'stream' and put them in 'quque'.
''' def __init__(self, stream):
'''
while True: stream: the stream to read from.
line = stream.readline() Usually a process' stdout or stderr.
if line: '''
queue.put(line)
else: self._s = stream
raise UnexpectedEndOfStream self._q = Queue()
self._t = Thread(target = _populateQueue, def _populateQueue(stream, queue):
args = (self._s, self._q)) '''
self._t.daemon = True Collect lines from 'stream' and put them in 'quque'.
self._t.start() #start collecting lines from the stream '''
def readline(self, timeout = None): while True:
try: line = stream.readline()
return self._q.get(block = timeout is not None, if line:
timeout = timeout) queue.put(line)
except Empty: else:
return None raise UnexpectedEndOfStream
self._t = Thread(target = _populateQueue,
args = (self._s, self._q))
self._t.daemon = True
self._t.start() #start collecting lines from the stream
def readline(self, timeout = None):
try:
return self._q.get(block = timeout is not None,
timeout = timeout)
except Empty:
return None
class UnexpectedEndOfStream(Exception): pass class UnexpectedEndOfStream(Exception): pass