Add Minecraft bridge
parent
1a31407625
commit
0c381a32f5
|
@ -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": "<strong>Starting Minecraft-Matrix bridge...</strong>"
|
||||
}
|
||||
)
|
||||
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())
|
|
@ -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}...", "<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 "The Minecraft server is live. The server is reacable at <code>mc.noordstar.me</code>.", "The minecraft server is live. The server is reacable at <code>mc.noordstar.me</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, 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())
|
|
@ -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
|
Loading…
Reference in New Issue