Move Python scripts to src/ folder
parent
94155b34a1
commit
5d86b17a01
|
@ -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")
|
|
@ -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())
|
|
@ -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())
|
|
@ -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
|
Loading…
Reference in New Issue