diff --git a/main.py b/main.py
new file mode 100644
index 0000000..4fedfdb
--- /dev/null
+++ b/main.py
@@ -0,0 +1,66 @@
+import asyncio
+import time
+import re
+
+from nio import AsyncClient, MatrixRoom, RoomMessageText
+import 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.MC_CHANNEL:
+ return
+ if event.sender == client.user_id:
+ return
+ if int(event.server_timestamp) < STARTUP_TIME:
+ return
+
+ # Determine platform
+ platform = 'Matrix'
+ if re.fullmatch(r"@_discord_\d+:t2bot\.io", event.sender):
+ platform = 'Discord'
+
+ # 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,
+ admin=(event.sender in config.MC_ADMINS),
+ platform=platform
+ )
+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.MC_CHANNEL,
+ 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(
+ activate_client(),
+ mc_wrapper.start(client, config.MC_CHANNEL)
+ )
+
+asyncio.run(start())
\ No newline at end of file
diff --git a/mc_wrapper.py b/mc_wrapper.py
new file mode 100644
index 0000000..8fddbe1
--- /dev/null
+++ b/mc_wrapper.py
@@ -0,0 +1,123 @@
+from subprocess import Popen, PIPE
+from typing import Union
+import asyncio
+import json
+import sys
+import re
+from nbsr import NonBlockingStreamReader as NBSR
+
+# 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):
+ 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]:
+ 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 "The Minecraft server is live. The server is reacable at mc.noordstar.me
.", "The minecraft server is live. The server is reacable at mc.noordstar.me
."
+
+ 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, author : str,
+ admin : bool = False, platform : str = 'Matrix'):
+ """
+ Send something back to the Minecraft terminal.
+ """
+ if admin and message.startswith('!'):
+ p.stdin.write((message[1:] + "\r\n").encode())
+ else:
+ msg = [
+ "",
+ dict(text="M", color="red"),
+ dict(text="D", color="aqua") if platform == 'Discord' else None,
+ dict(text=f" <{author}> {message}")
+ ]
+ p.stdin.write(
+ ("execute as @a run tellraw @s " + json.dumps([m for m in msg if m is not None]) + "\r\n").encode()
+ )
+ p.stdin.flush()
+
+if __name__ == '__main__':
+ asyncio.run(start())
\ No newline at end of file
diff --git a/nbsr.py b/nbsr.py
new file mode 100644
index 0000000..3b0787d
--- /dev/null
+++ b/nbsr.py
@@ -0,0 +1,39 @@
+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