bridgecord/scripts/minecraft.js

206 lines
7.1 KiB
JavaScript
Raw Normal View History

// This script aims to bridge Minecraft to a guild of your choice by piping
// the Minecraft output into this script. RCON isrequired for messages to
// be sent to Minecraft.
2022-02-11 15:22:10 +02:00
// Running example - working directory: project root:
// (cd ~/minecraft_server/ ; exec ~/minecraft_server/start.sh) | RCON_PASSWORD="your rcon password" node scripts/minecraft.js
import fetch from "node-fetch";
import { WebSocket } from "ws";
import Rcon from "rcon";
const TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InN0YWdpbmctbWluZWNyYWZ0LWJyaWRnZSIsImF2YXRhclVSTCI6bnVsbCwiZGlzY29yZElEIjoiMCIsImd1aWxkQWNjZXNzIjpbIjczNjI5MjUwOTEzNDc0OTgwNyJdLCJpc1N1cGVyVG9rZW4iOnRydWUsImlhdCI6MTY0NDQ1MzM4MX0.-XIBl6VLnXVwve9iqhWs51ABZkm1i_v1tS6X01SPk3U"; // A supertoken is required to send messages from Minecraft.
const TARGET_GUILD_ID = "_";
const TARGET_CHANNEL_ID = "_";
const ORIGIN = "http://localhost:4050";
const GATEWAY_ORIGIN = "ws://localhost:4050/gateway";
const messageSchema = { t: "number", d: "object" };
const messageTypes = {
HELLO: 0,
YOO: 1,
READY: 2,
EVENT: 3
};
const chatMessageRegex = /^\[(?:.*?)\]: \<(?<username>.*)\> (?<message>.*)/;
const joinNotificationRegex = /^\[(?:.*?)\]: (?<username>.*) joined the game/;
const leaveNotificationRegex = /^\[(?:.*?)\]: (?<username>.*) left the game/;
const rconConnection = new Rcon("localhost", "25575", process.env.RCON_PASSWORD);
export default class GatewayClient {
constructor(gatewayPath) {
this.gatewayPath = gatewayPath;
this.ws = null;
this.token = null;
this.user = null;
this.onEvent = (e) => {};
}
connect(token) {
if (!token)
token = this.token;
if (this.ws) {
console.log("gateway: connect() but connection already exists, killing existing connection...");
try {
this.ws.removeAllListeners();
this.ws.close();
this.ws = null;
} catch (e) {
console.log("gateway: error while closing existing connection - it might not be established yet");
}
}
console.log("gateway: connecting");
this.ws = new WebSocket(this.gatewayPath);
this.ws.on("message", (data, isBinary) => {
if (isBinary) {
console.warn("gateway: got binary data from server, ignoring...");
return;
}
let message = data.toString();
try {
message = JSON.parse(data);
} catch(e) {
console.warn("gateway: got invalid JSON from server (failed to parse), ignoring...");
return;
}
if (!this._checkMessageSchema(message)) {
console.warn("gateway: got invalid JSON from server (does not match schema), ignoring...");
return;
}
switch (message.t) {
case messageTypes.HELLO: {
console.log("gateway: HELLO");
this.ws.send(JSON.stringify({
t: messageTypes.YOO,
d: {
token
}
}));
break;
}
case messageTypes.READY: {
console.log("gateway: READY");
this.user = message.d.user;
break;
}
case messageTypes.EVENT: {
this.onEvent(message.d);
break;
}
default: {
console.warn("gateway: got invalid JSON from server (invalid type), ignoring...");
return;
}
}
});
this.ws.on("open", () => {
console.log("gateway: open");
});
this.ws.on("close", () => {
console.log("gateway: closed, reconnecting in 4000ms");
setTimeout(() => {
console.log("gateway: reconnecting");
this.connect(token);
}, 4000);
});
this.ws.on("error", (e) => {
console.error("gateway: error", e);
console.log("gateway: reconnecting in 4000ms due to previous error");
setTimeout(() => {
console.log("gateway: reconnecting");
this.connect(token);
}, 4000);
});
}
_checkMessageSchema(message) {
for (const [key, value] of Object.entries(message)) {
if (!messageSchema[key])
return false;
if (typeof value !== messageSchema[key])
return false;
}
return true;
}
}
async function sendBridgeMessageAs(guildId, channelId, content, username=undefined, avatarURL=undefined) {
return await fetch(`${ORIGIN}/api/v1/guilds/${guildId}/channels/${channelId}/messages/create`, {
method: "POST",
body: JSON.stringify({
content,
username,
avatarURL
}),
headers: {
"content-type": "application/json",
"authorization": TOKEN
}
});
}
async function sendMinecraftMessageAs(rcon, username, content) {
rcon.send(`tellraw @a ${JSON.stringify([
{ text: "[" },
{ text: `${username}`, color: "gray" },
{ text: "]" },
{ text: " " },
{ text: content },
])}`);
}
async function main() {
rconConnection.on("error", (e) => {
console.error("rcon: got error", e);
if (!rconConnection.hasAuthed) {
console.log("rcon: reconnecting in 5000ms due to error before hasAuthed (server might not be up yet?)");
setTimeout(() => {
rconConnection.connect();
}, 5000);
}
});
const gateway = new GatewayClient(GATEWAY_ORIGIN);
gateway.onEvent = (e) => {
if (e.eventType === "MESSAGE_CREATE" && e.message.channel_id === TARGET_CHANNEL_ID && e.message.guild_id === TARGET_GUILD_ID && !e.message.webhook_id) {
sendMinecraftMessageAs(rconConnection, e.message.author.username, e.message.content);
}
};
rconConnection.connect();
gateway.connect(TOKEN);
process.stdin.resume();
process.stdin.on("data", async (rawDataBuffer) => {
const stringData = rawDataBuffer.toString().trim();
console.log(stringData);
const joinResult = joinNotificationRegex.exec(stringData);
if (joinResult) {
await sendBridgeMessageAs(TARGET_GUILD_ID, TARGET_CHANNEL_ID, `**${joinResult.groups.username}** joined the game`, null, null);
return;
}
const leaveResult = leaveNotificationRegex.exec(stringData);
if (leaveResult) {
await sendBridgeMessageAs(TARGET_GUILD_ID, TARGET_CHANNEL_ID, `**${leaveResult.groups.username}** left the game`, null, null);
return;
}
const messageResult = chatMessageRegex.exec(stringData);
if (messageResult) {
await sendBridgeMessageAs(TARGET_GUILD_ID, TARGET_CHANNEL_ID, messageResult.groups.message, messageResult.groups.username, null);
return;
}
});
}
await main();