diff --git a/build_server.py b/src/build_server.py
similarity index 96%
rename from build_server.py
rename to src/build_server.py
index 68b7229..9f48491 100644
--- a/build_server.py
+++ b/src/build_server.py
@@ -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")
diff --git a/mc_wrapper.py b/src/mc_wrapper.py
similarity index 94%
rename from mc_wrapper.py
rename to src/mc_wrapper.py
index 136a8af..e355c6d 100644
--- a/mc_wrapper.py
+++ b/src/mc_wrapper.py
@@ -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}...", "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__':
+"""
+ 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}...", "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())
\ No newline at end of file
diff --git a/main.py b/src/mxclient.py
similarity index 93%
rename from main.py
rename to src/mxclient.py
index 651aef6..2440316 100644
--- a/main.py
+++ b/src/mxclient.py
@@ -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": "Starting Minecraft-Matrix bridge..."
- }
- )
- 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": "Starting Minecraft-Matrix bridge..."
+ }
+ )
+ 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())
\ No newline at end of file
diff --git a/nbsr.py b/src/nbsr.py
similarity index 64%
rename from nbsr.py
rename to src/nbsr.py
index 3b0787d..74f2a03 100644
--- a/nbsr.py
+++ b/src/nbsr.py
@@ -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
\ No newline at end of file