Compare commits

..

No commits in common. "b3ab94ef7867f559d7ab5429ec84bf8b7b971a05" and "947b454faff11bbd777a63576aaa9ba87818aef8" have entirely different histories.

12 changed files with 384 additions and 417 deletions

4
.gitignore vendored
View File

@ -1,7 +1,3 @@
# Minecraft-specific
server.jar
world/
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

View File

@ -7,14 +7,13 @@ WORKDIR /usr/src/app
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# # Prepare MC server # Prepare MC server
# COPY server.jar ./ COPY server.jar ./
# RUN java -jar server.jar --nogui RUN java -jar server.jar --nogui
COPY src/ ./src/ COPY . .
COPY main.py ./
# Buffer Python's stdout for debugging during runtime # Buffer Python's stdout for debugging during runtime
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
CMD ["python", "main.py"] CMD ["python", "main.py", "java", "-Xmx1024M", "-Xms1024M", "-jar", "server.jar", "nogui"]

View File

@ -6,36 +6,25 @@ This way, communication is enabled between a Minecraft server and a Matrix room.
## Setup ## Setup
1. Create a Docker image using the following command: To create a Docker image, go to this folder and run the following command:
``` ```
docker build -t matrix-mc-server ./ docker build -t matrix-mc-server ./
``` ```
2. Download the [latest Minecraft server jar file](https://www.minecraft.net/en-us/download/server). Then, once a Docker image has been created, you can run the following command to run a Minecraft server. In `config.py`, you can find additional environment variables you can insert to alter settings on the Minecraft server.
3. Open `config.yaml` and adjust it to however you like. ```
docker run --name mc-server \
-p 25565:25565 \
-v "<folder in which your store your Minecraft world>":/usr/src/app/world \
-e EULA=true \
-e MATRIX_HOMESERVER='<your matrix homeserver>' \
-e MATRIX_USERNAME='<matrix bridge client username>' \
-e MATRIX_PASSWORD='<matrix bridge client password>' \
-e MC_CHANNEL='<channel in which you communicate>' \
-e SERVER_ADDRESS='<ip address where players can connect to the server>' \
mc-bridge
```
**NOTE:** Make sure to set EULA to `true` otherwise the server will refuse to start. This should successfully launch your bridged Minecraft server.
4. Start the Docker container. You will need a few volumes:
- `/usr/src/app/config.yaml` - Location of your custom `config.yaml`
- `/usr/src/app/whitelist.json` - Location of your whitelist, if any.
- `/usr/src/app/world/` - Folder of your custom Minecraft world to load
- `/usr/src/app/server.jar` - (Default) location of the server jar file. Can be changed in `config.yaml`.
For example, your Docker compose file could look like the following:
```docker-compose
version: '3'
services:
matrix-mc-server:
image: matrix-mc-server:latest
volumes:
- <your config>:/usr/src/app/config.yaml
- <your whitelist>:/usr/src/app/whitelist.json
- <your world folder>:/usr/src/app/world
- <your server jar file>:/usr/src/app/server.jar
```

48
build_server.py Normal file
View File

@ -0,0 +1,48 @@
import os
import config
def rewriter(file_name):
"""Create a decorator that rewrites a file based on given settings."""
def exec(func):
"""Rewrite a file"""
new_lines = []
info_to_remember = {}
line_no = 0
with open(file_name, 'r', encoding='utf-8') as open_file:
for line in open_file:
line_no += 1
new_line = func(line, line_no, data=info_to_remember)
new_lines.append(new_line)
with open(file_name, 'w', encoding='utf-8') as write_file:
for line in new_lines:
write_file.write(line)
return exec
@rewriter('eula.txt')
def confirm_eula(line : str, line_no : int, data):
"""Confirm the Minecraft EULA"""
if not config.EULA:
return line
else:
return line.replace('eula=false', 'eula=true')
@rewriter('server.properties')
def fill_in_server_settings(line : str, line_no : int, data):
"""Set up the server based on our chosen properties"""
if line.strip().startswith('#'):
return line
key = line.split('=')[0]
server_settings = config.at(['minecraft']) or {}
try:
value = server_settings[key]
except IndexError:
return line
else:
return key + '=' + str(value) + '\n'

84
config.py Normal file
View File

@ -0,0 +1,84 @@
import os
import yaml
from typing import Any, List, Optional
with open('config.yaml', 'r') as open_file:
SETTINGS = yaml.load(open_file)
def at(keys : List[str]) -> Optional[Any]:
"""
Potentially get a value. If it doesn't exist, return None.
"""
return at_value(keys, SETTINGS)
def at_value(keys : List[str], value : Any) -> Optional[Any]:
try:
head, tail = keys[0], keys[1:]
except IndexError:
return value
else:
try:
new_value = value[head]
except TypeError:
return None
except KeyError:
return None
else:
return at_value(tail, new_value)
# EULA
EULA = at(['minecraft', 'eula']) or False
# Minecraft bridge credentials
MATRIX_HOMESERVER = at(['matrix', 'homeserver']) or "https://matrix.example.org/"
MATRIX_USERNAME = at(['matrix', 'username']) or "@alice:example.org"
MATRIX_PASSWORD = at(['matrix', 'password']) or "bridge_password"
# Matrix bridge room
MATRIX_ROOM = at(['matrix', 'room_id']) or "!channel_id:example.org"
SERVER_IP = os.getenv('SERVER_ADDRESS') or 'unknown ip'
# Matrix users who are allowed to run OP commands in Minecraft through Matrix
MC_ADMINS = [
"@bramvdnheuvel:nltrix.net", # Bram on Matrix (example, feel free to remove)
"@_discord_625632515314548736:t2bot.io" # Bram on Discord (example, feel free to remove)
# Your username on Matrix
]
if os.getenv('MATRIX_ADMINS') is not None:
MC_ADMINS = os.getenv('MATRIX_ADMINS').split(',')
make_bool = lambda os_value, default_value : default_value if not os_value else (
False if os_value.lower() == 'false' else True
)
SERVER_SETTINGS = {
'level-name': os.getenv('WORLD') or 'world',
# Server settings
'port' : 25565 if os.getenv('PORT') == None else int(os.getenv('PORT')),
'query.port' : 25565 if os.getenv('PORT') == None else int(os.getenv('PORT')),
'max-players' : 7 if os.getenv('MAX_PLAYERS') == None else int(os.getenv('MAX_PLAYERS')),
# Server temperature >:3
'view-distance' : 10 if os.getenv('RENDER_DISTANCE') == None else int(os.getenv('RENDER_DISTANCE')),
'enable-command-block' : make_bool(os.getenv('COMMAND_BLOCKS'), True),
# Environment
'allow-nether' : make_bool(os.getenv('NETHER'), True),
'spawn-npcs' : make_bool(os.getenv('NPCS'), True),
'spawn-animals' : make_bool(os.getenv('ANIMALS'), True),
'spawn-monsters' : make_bool(os.getenv('MONSTERS'), True),
# Gamemode
'pvp' : make_bool(os.getenv('PVP'), True),
'gamemode' : os.getenv('GAMEMODE') or 'survival',
'difficulty': os.getenv('DIFFICULTY') or 'medium',
'hardcore' : make_bool(os.getenv('HARDCORE'), False),
# Grief protection
'online-mode' : make_bool(os.getenv('VERIFY_ACCOUNTS'), True),
'white-list' : make_bool(os.getenv('WHITELIST'), True),
'enforce-whitelist' : make_bool(os.getenv('WHITELIST'), True),
'spawn-protection' : 16 if os.getenv('SPAWN_PROTECTION') == None else os.getenv('SPAWN_PROTECTION'),
}

View File

@ -1,16 +1,3 @@
config:
# How much memory the Minecraft server is allowed to take up
# Number in megabytes. Defaults to 1024. (= 1 GB)
ram: 1024
# File location of the server jar.
# To be downloaded at: https://www.minecraft.net/en-us/download/server
server_jar: server.jar
# Confirm the Minecraft EULA. https://account.mojang.com/documents/minecraft_eula
# Defaults to false, but is required to run the server.
# eula: true
# Matrix bridge configurations # Matrix bridge configurations
matrix: matrix:
# Homeserver URL # Homeserver URL
@ -27,16 +14,14 @@ matrix:
server_address: unknown ip server_address: unknown ip
# List of Matrix users that can send commands to the bridge. # List of Matrix users that can send commands to the bridge.
# When a message starts with an exclamation mark, (!) the bridge will # When a message starts with a slash, (/) the bridge will interpret it as a
# interpret it as a Minecraft command and will put that as a command # Minecraft command and will put that as a command into the console.
# into the console.
mc-admins: mc-admins:
- "@bram:matrix.directory" - "@bram:matrix.directory"
# - "@alice:example.org" # - "@alice:example.org"
# - # -
# When users have bridged from other platforms, you can indicate accordingly. # When users have bridged from other platforms, you can indicate accordingly.
# When multiple RegEx strings apply, all are included.
alternative_platforms: alternative_platforms:
Discord: Discord:
match: "@_?discord_\d+:.+" match: "@_?discord_\d+:.+"
@ -49,6 +34,8 @@ matrix:
# Settings that directly affect running the Minecraft server. # Settings that directly affect running the Minecraft server.
minecraft: minecraft:
# Confirm the Minecraft EULA. Defaults to false.
# eula: true
# -------------------- # --------------------
# MINECRAFT SERVER # MINECRAFT SERVER

74
main.py
View File

@ -1,7 +1,67 @@
import src.mc_wrapper as mc_wrapper import asyncio
import src.mxclient as matrix_client import time
import re
import asyncio
from nio import AsyncClient, MatrixRoom, RoomMessageText
# Start the Minecraft process import mc_wrapper
asyncio.run(matrix_client.start())
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 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())

View File

@ -1,160 +1,124 @@
""" from subprocess import Popen, PIPE
The mc_wrapper modules handles the process that runs the Minecraft server. from typing import Union
import asyncio
The server is run in a subprocess, and live communication between the import json
game and the Python script is facilitated. import sys
""" import re
from nbsr import NonBlockingStreamReader as NBSR
from subprocess import Popen, PIPE import config
from typing import Union
import asyncio # run the shell as a subprocess:
import json p = Popen(sys.argv[1:],
import re stdin = PIPE, stdout = PIPE, stderr = PIPE, shell = False)
from src.nbsr import NonBlockingStreamReader as NBSR # wrap p.stdout with a NonBlockingStreamReader object:
import src.config as config nbsr = NBSR(p.stdout)
import src.build_server as build
async def start(client, mc_channel):
# Write the appropriate files await asyncio.sleep(3)
build.write_eula()
build.write_server_properties() while True:
output = nbsr.readline(0.1)
# run the shell as a subprocess: # 0.1 secs to let the shell output the result
p = Popen(config.RUN_COMMAND, if not output:
stdin = PIPE, stdout = PIPE, stderr = PIPE, shell = False) await asyncio.sleep(1)
# wrap p.stdout with a NonBlockingStreamReader object: else:
nbsr = NBSR(p.stdout) try:
sentence = output.decode("utf-8").strip()
async def start(client, mc_channel): except UnicodeDecodeError:
""" print("Could not decode sentence:")
Start reading from the Minecraft subprocess. print(output)
""" else:
await asyncio.sleep(3) print(sentence)
plain_text, sentence = process_message(sentence)
while True:
output = nbsr.readline(0.1) if sentence is not None:
# 0.1 secs to let the shell output the result print("[Matrix] " + plain_text)
if not output:
await asyncio.sleep(1) # Send terminal message to Matrix
else: await client.room_send(
try: room_id=mc_channel,
sentence = output.decode("utf-8").strip() message_type="m.room.message",
except UnicodeDecodeError: content = {
print("Could not decode sentence:") "msgtype": "m.text",
print(output) "body": plain_text,
else: "format": "org.matrix.custom.html",
print(sentence) "formatted_body": sentence
plain_text, sentence = process_message(sentence) }
)
if sentence is not None:
print("[Matrix] " + plain_text) server_live = False
# Send terminal message to Matrix def process_message(sentence : str) -> Union[str, None]:
await client.room_send( global server_live
room_id=mc_channel,
message_type="m.room.message", if (match := re.fullmatch(
content = { r"\[[\d:]+\] \[Server thread\/INFO\]: Preparing level \"(.+)\""
"msgtype": "m.text", , sentence)):
"body": plain_text, level, = match.groups()
"format": "org.matrix.custom.html", return f"Preparing level {level}...", "<strong>Preparing level \"" + level + "\"...</strong>"
"formatted_body": sentence
} if re.fullmatch(
) r"\[[\d:]+\] \[Server thread\/INFO\]: Done \(\d+.?\d*s\)! For help, type \"help\"",
sentence):
server_live = False 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>."
def process_message(sentence : str) -> Union[str, None]:
""" if re.fullmatch(
Process a message that is sent to stdout in the Minecraft terminal. r"\[[\d:]+\] \[Server thread\/INFO\]: Stopping server",
sentence):
If this function deems it relevant, it returns a string that can be return "The server has stopped.", "<strong>The server has stopped.</strong>"
sent to Matrix.
""" if not server_live:
global server_live return None, None
if (match := re.fullmatch( if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: Preparing level \"(.+)\"" r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) joined the game",
, sentence)): sentence)):
level, = match.groups() username, = match.groups()
return f"Preparing level {level}...", "<strong>Preparing level \"" + level + "\"...</strong>" return username + " joined the Minecraft server", (
"<strong>" + username + " joined the Minecraft server</strong>")
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\]: ([A-Za-z0-9_]{3,16}) left the game",
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>." username, = match.groups()
return username + " left the Minecraft server", (
if re.fullmatch( "<strong>" + username + " left the Minecraft server</strong>")
r"\[[\d:]+\] \[Server thread\/INFO\]: Stopping server",
sentence): if (match := re.fullmatch(
return "The server has stopped.", "<strong>The server has stopped.</strong>" r"\[[\d:]+\] \[Server thread\/INFO\]: <([A-Za-z0-9_]{3,16})> (.+)",
sentence)):
if not server_live: username, message = match.groups()
return None, None return username + ": " + message, (
"<strong>" + username + "</strong>: " + message
if (match := re.fullmatch( )
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) joined the game",
sentence)): if (match := re.fullmatch(
username, = match.groups() r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) ([\w\[\]\-\. !?,]+)",
return username + " joined the Minecraft server", ( sentence)):
"<strong>" + username + " joined the Minecraft server</strong>") message = " ".join(match.groups())
return message, "<strong>" + message + "</strong>"
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) left the game", return None, None
sentence)):
username, = match.groups() def reply_to_mc(message : str, author : str,
return username + " left the Minecraft server", ( admin : bool = False, platform : str = 'Matrix'):
"<strong>" + username + " left the Minecraft server</strong>" """
) Send something back to the Minecraft terminal.
"""
if (match := re.fullmatch( if admin and message.startswith('!'):
r"\[[\d:]+\] \[Server thread\/INFO\]: <([A-Za-z0-9_]{3,16})> (.+)", p.stdin.write((message[1:] + "\r\n").encode())
sentence)): else:
username, message = match.groups() msg = [
return username + ": " + message, ( "",
"<strong>" + username + "</strong>: " + message dict(text="M", color="red"),
) dict(text="D", color="aqua") if platform == 'Discord' else None,
dict(text=f" <{author}> {message}")
if (match := re.fullmatch( ]
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) ([\w\[\]\-\. !?,]+)", p.stdin.write(
sentence)): ("execute as @a run tellraw @s " + json.dumps([m for m in msg if m is not None]) + "\r\n").encode()
message = " ".join(match.groups()) )
return message, "<strong>" + message + "</strong>" p.stdin.flush()
return None, None if __name__ == '__main__':
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()) asyncio.run(start())

View File

@ -1,51 +1,39 @@
""" from threading import Thread
The NBSR module defines a Non-blocking stream reader (NBSR class). from queue import Queue, Empty
In short, the Minecraft stdout is a stream of data that doesn't end until class NonBlockingStreamReader:
the server shuts down. Traditionally, Python would not run any code until
the server has shut down and returns its entire output. def __init__(self, stream):
'''
The NBSR class allows us to read from the stream without blocking the entire stream: the stream to read from.
Python script. We will occasionally ask the NBSR for any updates, and it Usually a process' stdout or stderr.
will give us the latest output, if it exists. '''
"""
self._s = stream
from threading import Thread self._q = Queue()
from queue import Queue, Empty
def _populateQueue(stream, queue):
class NonBlockingStreamReader: '''
Collect lines from 'stream' and put them in 'quque'.
def __init__(self, stream): '''
'''
stream: the stream to read from. while True:
Usually a process' stdout or stderr. line = stream.readline()
''' if line:
queue.put(line)
self._s = stream else:
self._q = Queue() raise UnexpectedEndOfStream
def _populateQueue(stream, queue): self._t = Thread(target = _populateQueue,
''' args = (self._s, self._q))
Collect lines from 'stream' and put them in 'quque'. self._t.daemon = True
''' self._t.start() #start collecting lines from the stream
while True: def readline(self, timeout = None):
line = stream.readline() try:
if line: return self._q.get(block = timeout is not None,
queue.put(line) timeout = timeout)
else: except Empty:
raise UnexpectedEndOfStream return None
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

View File

@ -1,25 +0,0 @@
"""
This module prepares the necessary files for running the server in the
correct configuration.
"""
import src.config as 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,62 +0,0 @@
"""
This module loads and parses the config.yaml file.
"""
from typing import Any, List, Optional
import yaml
with open('config.yaml', 'r') as open_file:
SETTINGS = yaml.load(open_file)
def at(keys : List[str]) -> Optional[Any]:
"""
Potentially get a value. If it doesn't exist, return None.
"""
return at_value(keys, SETTINGS)
def at_value(keys : List[str], value : Any) -> Optional[Any]:
try:
head, tail = keys[0], keys[1:]
except IndexError:
return value
else:
try:
new_value = value[head]
except TypeError:
return None
except KeyError:
return None
else:
return at_value(tail, new_value)
# EULA
EULA = at(['config', 'eula']) or False
# Minecraft bridge credentials
MATRIX_HOMESERVER = at(['matrix', 'homeserver']) or "https://matrix.example.org/"
MATRIX_USERNAME = at(['matrix', 'username']) or "@alice:example.org"
MATRIX_PASSWORD = at(['matrix', 'password']) or "bridge_password"
# Matrix bridge room
MATRIX_ROOM = at(['matrix', 'room_id']) or "!channel_id:example.org"
SERVER_IP = at(['matrix', 'server_address']) or 'unknown ip'
MATRIX_ADMINS = at(['matrix', 'mc-admins']) or []
try:
RAM_SIZE = int(at(['config', 'ram']))
except TypeError:
RAM_SIZE = 1024
except ValueError:
RAM_SIZE = 1024
SERVER_JAR_LOCATION = at(['config', 'server_jar']) or 'server.jar'
RUN_COMMAND = [
'java',
f'-Xmx{RAM_SIZE}M',
f'-Xms{RAM_SIZE}M',
'-jar', SERVER_JAR_LOCATION,
'nogui'
]

View File

@ -1,61 +0,0 @@
import asyncio
import time
import re
from nio import AsyncClient, MatrixRoom, RoomMessageText
import src.mc_wrapper as mc_wrapper
import src.config as 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)
)
if __name__ == '__main__':
asyncio.run(start())