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

View File

@ -1,153 +1,160 @@
from subprocess import Popen, PIPE
from typing import Union
import asyncio
import json
import re
from nbsr import NonBlockingStreamReader as NBSR
import config
import build_server as build
# Write the appropriate files
build.write_eula()
build.write_server_properties()
# run the shell as a subprocess:
p = Popen(config.RUN_COMMAND,
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}...", "<strong>Preparing level \"" + level + "\"...</strong>"
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 <code>{config.SERVER_IP}</code>.", f"The minecraft server is live. The server is reacable at <code>{config.SERVER_IP}</code>."
if re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: Stopping server",
sentence):
return "The server has stopped.", "<strong>The server has stopped.</strong>"
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", (
"<strong>" + username + " joined the Minecraft server</strong>")
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", (
"<strong>" + username + " left the Minecraft server</strong>"
)
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: <([A-Za-z0-9_]{3,16})> (.+)",
sentence)):
username, message = match.groups()
return username + ": " + message, (
"<strong>" + username + "</strong>: " + message
)
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) ([\w\[\]\-\. !?,]+)",
sentence)):
message = " ".join(match.groups())
return message, "<strong>" + message + "</strong>"
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__':
"""
The mc_wrapper modules handles the process that runs the Minecraft server.
The server is run in a subprocess, and live communication between the
game and the Python script is facilitated.
"""
from subprocess import Popen, PIPE
from typing import Union
import asyncio
import json
import re
from src.nbsr import NonBlockingStreamReader as NBSR
import config
import src.build_server as build
# Write the appropriate files
build.write_eula()
build.write_server_properties()
# run the shell as a subprocess:
p = Popen(config.RUN_COMMAND,
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}...", "<strong>Preparing level \"" + level + "\"...</strong>"
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 <code>{config.SERVER_IP}</code>.", f"The minecraft server is live. The server is reacable at <code>{config.SERVER_IP}</code>."
if re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: Stopping server",
sentence):
return "The server has stopped.", "<strong>The server has stopped.</strong>"
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", (
"<strong>" + username + " joined the Minecraft server</strong>")
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", (
"<strong>" + username + " left the Minecraft server</strong>"
)
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: <([A-Za-z0-9_]{3,16})> (.+)",
sentence)):
username, message = match.groups()
return username + ": " + message, (
"<strong>" + username + "</strong>: " + message
)
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) ([\w\[\]\-\. !?,]+)",
sentence)):
message = " ".join(match.groups())
return message, "<strong>" + message + "</strong>"
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())

View File

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

View File

@ -1,39 +1,51 @@
from threading import Thread
from queue import Queue, Empty
class NonBlockingStreamReader:
def __init__(self, stream):
'''
stream: the stream to read from.
Usually a process' stdout or stderr.
'''
self._s = stream
self._q = Queue()
def _populateQueue(stream, queue):
'''
Collect lines from 'stream' and put them in 'quque'.
'''
while True:
line = stream.readline()
if line:
queue.put(line)
else:
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
"""
The NBSR module defines a Non-blocking stream reader (NBSR class).
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
the server has shut down and returns its entire output.
The NBSR class allows us to read from the stream without blocking the entire
Python script. We will occasionally ask the NBSR for any updates, and it
will give us the latest output, if it exists.
"""
from threading import Thread
from queue import Queue, Empty
class NonBlockingStreamReader:
def __init__(self, stream):
'''
stream: the stream to read from.
Usually a process' stdout or stderr.
'''
self._s = stream
self._q = Queue()
def _populateQueue(stream, queue):
'''
Collect lines from 'stream' and put them in 'quque'.
'''
while True:
line = stream.readline()
if line:
queue.put(line)
else:
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