diff --git a/GatewayServer.js b/GatewayServer.js index 47c70ae..562f55a 100644 --- a/GatewayServer.js +++ b/GatewayServer.js @@ -1,4 +1,3 @@ -import { use } from "express/lib/application"; import { WebSocketServer } from "ws"; import { guildMap } from "./common.js"; import { decodeToken } from "./tokens.js"; diff --git a/LICENSE b/LICENSE index 117a364..b4c3184 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 hippoz +Copyright (c) 2022 hippoz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/config.js b/config.js index 91a5f98..2158dca 100644 --- a/config.js +++ b/config.js @@ -1,5 +1,5 @@ export const mainHttpListenPort = 4050; -export const watchedGuildIds = ["822089558886842418"]; +export const watchedGuildIds = ["822089558886842418", "736292509134749807"]; export const jwtSecret = process.env.JWT_SECRET; export const discordToken = process.env.DISCORD_TOKEN; export const dangerousAdminMode = true; diff --git a/package.json b/package.json index 305b76d..e4e72a0 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ "jsonwebtoken": "^8.5.1", "node-fetch": "^3.2.0", "ws": "^8.4.2" + }, + "optionalDependencies": { + "rcon": "^1.1.0" } } diff --git a/routes/api.js b/routes/api.js index 268844a..2f45474 100644 --- a/routes/api.js +++ b/routes/api.js @@ -31,7 +31,7 @@ router.get("/users/@self", checkAuth(async (req, res) => { avatarURL: req.user.avatarURL, discordID: req.user.discordID, guildAccess: req.user.guildAccess, - isSuperToken: isSuperToken + isSuperToken: req.user.isSuperToken }}); })); @@ -61,6 +61,10 @@ router.post("/guilds/:guildId/channels/:channelId/messages/create", checkAuth(as return res.status(403).send({ error: true, message: "ERROR_NO_GUILD_ACCESS" }); const guild = guildMap.get(guildId); + + if (!guild) + return res.status(404).send({ error: true, message: "ERROR_GUILD_NOT_FOUND" }); + try { await guild.discordSendMessage(messageContent, channelId, username, avatarURL); res.status(201).send({ error: false }); diff --git a/scripts/minecraft.js b/scripts/minecraft.js new file mode 100644 index 0000000..42e5415 --- /dev/null +++ b/scripts/minecraft.js @@ -0,0 +1,169 @@ +// 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. + +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 = /^\[(?:.*?)\]: \<(?.*)\> (?.*)/; +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; + + 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"); + 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 1200ms due to error before hasAuthed"); + setTimeout(() => { + rconConnection.connect(); + }, 1200); + } + }); + 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 result = chatMessageRegex.exec(stringData); + if (!result) + return; + const { username, message } = result.groups; + + await sendBridgeMessageAs(TARGET_GUILD_ID, TARGET_CHANNEL_ID, message, username, null); + }); +} + +await main(); diff --git a/yarn.lock b/yarn.lock index 8b18752..91276e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -388,6 +388,11 @@ raw-body@2.4.2: iconv-lite "0.4.24" unpipe "1.0.0" +rcon@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/rcon/-/rcon-1.1.0.tgz#82a27bbfadd4c13b3c5d828b55ce15bd606eb7c3" + integrity sha512-eotwcApOBjfadTjqQlrZVR4jzlwGCMNxmHhnFZx+g4kouwwRstRHkk1ON7DzkqrHNIjADSh0cU3gThSsDolUpg== + safe-buffer@5.2.1, safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"