initial commit
This commit is contained in:
commit
cc2b2a3678
6 changed files with 678 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules/
|
291
DiscordClient.js
Normal file
291
DiscordClient.js
Normal file
|
@ -0,0 +1,291 @@
|
|||
import EventEmitter from "events";
|
||||
import { WebSocket } from "ws";
|
||||
import fetch from "node-fetch";
|
||||
import { logger } from "./common.js";
|
||||
|
||||
const log = logger("log", "DiscordClient");
|
||||
const logError = logger("error", "DiscordClient");
|
||||
const logWarn = logger("warn", "DiscordClient");
|
||||
|
||||
const opcodes = {
|
||||
EVENT: 0,
|
||||
CLIENT_HEARTBEAT: 1,
|
||||
IDENTIFY: 2,
|
||||
RECONNECT: 7,
|
||||
INVALID_SESSION: 9,
|
||||
HELLO: 10,
|
||||
HEARTBEAT_ACK: 11,
|
||||
};
|
||||
|
||||
const skipReconnectFor = [
|
||||
4004, 4010, 4011, 4012, 4013, 4014
|
||||
];
|
||||
|
||||
const CLOSE_CONNECTION_ON_NO_ACK = false;
|
||||
|
||||
class DiscordClient extends EventEmitter {
|
||||
constructor(token, { intents, gatewayUrl="wss://gateway.discord.gg/?v=10&encoding=json", apiBase="https://discord.com/api/v10" } = {}) {
|
||||
super();
|
||||
|
||||
this.token = token;
|
||||
this.gatewayUrl = gatewayUrl;
|
||||
this.apiBase = apiBase;
|
||||
this.ws = null;
|
||||
this.intents = intents;
|
||||
|
||||
this.user = null;
|
||||
this.guilds = [];
|
||||
this.sessionId = null;
|
||||
this.seq = null;
|
||||
this.gotServerHeartbeatACK = true;
|
||||
}
|
||||
|
||||
_setHeartbeat(interval) {
|
||||
this._heartbeatIntervalTime = interval;
|
||||
if (interval < 0 && this._heartbeatInterval) {
|
||||
clearInterval(this._heartbeatInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
this._heartbeatInterval = setInterval(() => {
|
||||
if (CLOSE_CONNECTION_ON_NO_ACK && !this.gotServerHeartbeatACK) {
|
||||
logError("Closing due to no heartbeat ACK...");
|
||||
this.ws.close(1000, "No heartbeat ACK.");
|
||||
return;
|
||||
}
|
||||
this.gotServerHeartbeatACK = false;
|
||||
|
||||
this.ws.send(JSON.stringify({
|
||||
op: opcodes.CLIENT_HEARTBEAT,
|
||||
d: this.seq
|
||||
}));
|
||||
}, this._heartbeatIntervalTime);
|
||||
}
|
||||
|
||||
_getIdentifyPayload() {
|
||||
return {
|
||||
token: this.token,
|
||||
intents: this.intents,
|
||||
properties: {
|
||||
"$os": "linux",
|
||||
"$browser": "generic",
|
||||
"$device": "generic"
|
||||
},
|
||||
presence: {
|
||||
since: Date.now(),
|
||||
activities: [
|
||||
{
|
||||
type: 2, // LISTENING
|
||||
name: "the voices"
|
||||
}
|
||||
],
|
||||
status: "online",
|
||||
afk: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_handleGatewayMessage(ws, message) {
|
||||
try {
|
||||
message = JSON.parse(message);
|
||||
} catch(e) {
|
||||
logError("on 'message': failed to parse incoming message as JSON", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.s) {
|
||||
this.seq = message.s;
|
||||
}
|
||||
|
||||
const payload = message.d;
|
||||
|
||||
switch (message.op) {
|
||||
case opcodes.HELLO: {
|
||||
log(`HELLO; heartbeat_interval=${payload.heartbeat_interval}`);
|
||||
this._setHeartbeat(payload.heartbeat_interval);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
op: opcodes.IDENTIFY,
|
||||
d: this._getIdentifyPayload()
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case opcodes.EVENT: {
|
||||
switch (message.t) {
|
||||
case "READY": {
|
||||
this.user = payload.user;
|
||||
this.sessionId = payload.session_id;
|
||||
this.guilds = payload.guilds;
|
||||
log(`READY. Connected as '${this.user.username}', got ${this.guilds.length} guilds.`);
|
||||
break;
|
||||
}
|
||||
|
||||
case "GUILD_CREATE": {
|
||||
const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id);
|
||||
if (targetGuildIndex < 0) {
|
||||
this.guilds.push(payload);
|
||||
break;
|
||||
}
|
||||
// The guild already exists in our array. This means that
|
||||
// this GUILD_CREATE event is completing our `Unavailable Guild`
|
||||
// objects that we got from the initial READY.
|
||||
this.guilds[targetGuildIndex] = payload;
|
||||
break;
|
||||
}
|
||||
|
||||
case "GUILD_UPDATE": {
|
||||
const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id);
|
||||
if (targetGuildIndex < 0) {
|
||||
// tried to update a guild that doesn't exist???
|
||||
this.emit("warn", "got GUILD_UPDATE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
this.guilds[targetGuildIndex] = payload;
|
||||
break;
|
||||
}
|
||||
|
||||
case "GUILD_DELETE": {
|
||||
const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id);
|
||||
if (targetGuildIndex < 0) {
|
||||
// tried to delete a guild that doesn't exist???
|
||||
this.emit("warn", "got GUILD_DELETE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
this.guilds.splice(targetGuildIndex, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case "CHANNEL_CREATE": {
|
||||
const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id);
|
||||
if (parentGuildIndex < 0) {
|
||||
this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
this.guilds[parentGuildIndex].channels.push(payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case "CHANNEL_UPDATE": {
|
||||
const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id);
|
||||
if (parentGuildIndex < 0) {
|
||||
this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
const relevantChannelIndex = this.guilds[parentGuildIndex].channels.findIndex(e => e.id === payload.id);
|
||||
this.guilds[parentGuildIndex].channels[relevantChannelIndex] = payload;
|
||||
break;
|
||||
}
|
||||
|
||||
case "CHANNEL_DELETE": {
|
||||
const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id);
|
||||
if (parentGuildIndex < 0) {
|
||||
this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
const relevantChannelIndex = this.guilds[parentGuildIndex].channels.findIndex(e => e.id === payload.id);
|
||||
this.guilds[parentGuildIndex].channels.splice(relevantChannelIndex, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.emit(message.t, payload);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case opcodes.HEARTBEAT_ACK: {
|
||||
this.gotServerHeartbeatACK = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case opcodes.INVALID_SESSION: {
|
||||
logError("INVALID_SESSION - please check your authentication token");
|
||||
logError("INVALID_SESSION: will not reconnect");
|
||||
break;
|
||||
}
|
||||
|
||||
case opcodes.RECONNECT: {
|
||||
log("gateway is requesting reconnect (payload RECONNECT)");
|
||||
this.connect();
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
logWarn(`got unhandled opcode "${message.op}"`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
log("connecting...");
|
||||
if (this.ws) {
|
||||
log("a websocket connection already exists, killing...");
|
||||
this.ws.removeAllListeners();
|
||||
this._setHeartbeat(-1);
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(this.gatewayUrl);
|
||||
this.ws = ws;
|
||||
|
||||
ws.on("message", (data, isBinary) => {
|
||||
if (!isBinary) {
|
||||
this._handleGatewayMessage(ws, data.toString());
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("open", () => {
|
||||
log("WebSocket 'open'");
|
||||
});
|
||||
|
||||
ws.on("close", (code, reason) => {
|
||||
reason = reason.toString();
|
||||
logError(`on 'close': disconnected from gateway: code '${code}', reason '${reason}'`);
|
||||
|
||||
this.emit("close", code, reason);
|
||||
this._setHeartbeat(-1);
|
||||
if (skipReconnectFor.includes(code)) {
|
||||
logError("on 'close': the exit code above is in skipReconnectFor, and thus the server will not reconnect.");
|
||||
} else {
|
||||
log("on 'close': the client will now attempt to reconnect...");
|
||||
this.connect();
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("error", (e) => {
|
||||
logError("on 'error': websocket error:", e);
|
||||
log("on 'error': reconnecting due to previous websocket error...");
|
||||
this._setHeartbeat(-1);
|
||||
this.connect();
|
||||
});
|
||||
}
|
||||
|
||||
async api(method, path, body=undefined, throwOnError=true) {
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
"authorization": `Bot ${this.token}`
|
||||
}
|
||||
};
|
||||
|
||||
if (method !== "GET" && method !== "HEAD" && typeof body === "object") {
|
||||
options.headers["content-type"] = "application/json";
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}${path}`, options);
|
||||
const json = await response.json();
|
||||
|
||||
if (!response.ok && throwOnError) {
|
||||
throw new Error(`API request returned non-success status ${response.status}, with JSON body ${JSON.stringify(json)}`);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
export default DiscordClient;
|
43
common.js
Normal file
43
common.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
const logContextMap = {
|
||||
DiscordClient: {
|
||||
log: true,
|
||||
warn: true,
|
||||
error: true
|
||||
},
|
||||
Bridge: {
|
||||
log: true,
|
||||
warn: true,
|
||||
error: true
|
||||
}
|
||||
};
|
||||
|
||||
export function logger(sink, context) {
|
||||
let sinkFunction;
|
||||
switch (sink) {
|
||||
case "log": {
|
||||
sinkFunction = console.log;
|
||||
break;
|
||||
}
|
||||
case "warn": {
|
||||
sinkFunction = console.warn;
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
sinkFunction = console.error;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
sinkFunction = () => {};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (logContextMap[context] && logContextMap[context][sink]) {
|
||||
return (...e) => {
|
||||
sinkFunction(`[${context}]`, ...e);
|
||||
};
|
||||
} else {
|
||||
return (...e) => {};
|
||||
}
|
||||
}
|
||||
|
285
index.js
Normal file
285
index.js
Normal file
|
@ -0,0 +1,285 @@
|
|||
import fetch from "node-fetch";
|
||||
import { spawn } from "node:child_process";
|
||||
import DiscordClient from "./DiscordClient.js";
|
||||
import { logger } from "./common.js";
|
||||
|
||||
const log = logger("log", "Bridge");
|
||||
const logError = logger("error", "Bridge");
|
||||
|
||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
|
||||
const WEBHOOK_URL = process.env.WEBHOOK_URL;
|
||||
const TARGET_GUILD_ID = process.env.TARGET_GUILD_ID;
|
||||
const TARGET_CHANNEL_ID = process.env.TARGET_CHANNEL_ID;
|
||||
const SERVER_JAR = process.env.SERVER_JAR;
|
||||
const SERVER_PWD = process.env.SERVER_PWD;
|
||||
const requiredEnv = { DISCORD_TOKEN, WEBHOOK_URL, TARGET_GUILD_ID, TARGET_CHANNEL_ID, SERVER_JAR, SERVER_PWD };
|
||||
|
||||
|
||||
const chatMessageRegex = /^\[(?:.*?)\]: \<(?<username>[a-zA-Z0-9]+)\> (?<message>.*)/;
|
||||
const joinNotificationRegex = /^\[(?:.*?)\]: (?<username>[a-zA-Z0-9]+) joined the game/;
|
||||
const leaveNotificationRegex = /^\[(?:.*?)\]: (?<username>[a-zA-Z0-9]+) left the game/;
|
||||
|
||||
const createServerChildProcess = () => {
|
||||
return spawn("java", [
|
||||
"-Xms1G", "-Xmx5G", "-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled", "-XX:MaxGCPauseMillis=200", "-XX:+UnlockExperimentalVMOptions", "-XX:+DisableExplicitGC", "-XX:+AlwaysPreTouch", "-XX:G1NewSizePercent=30",
|
||||
"-XX:G1MaxNewSizePercent=40", "-XX:G1HeapRegionSize=8M", "-XX:G1ReservePercent=20", "-XX:G1HeapWastePercent=5", "-XX:G1MixedGCCountTarget=4", "-XX:InitiatingHeapOccupancyPercent=15", "-XX:G1MixedGCLiveThresholdPercent=90",
|
||||
"-XX:G1RSetUpdatingPauseTimePercent=5", "-XX:SurvivorRatio=32", "-XX:+PerfDisableSharedMem", "-XX:MaxTenuringThreshold=1", "-Dusing.aikars.flags=https://mcflags.emc.gs",
|
||||
"-Daikars.new.flags=true", "-jar", SERVER_JAR, "nogui"
|
||||
], {
|
||||
cwd: SERVER_PWD
|
||||
});
|
||||
};
|
||||
|
||||
class Bridge {
|
||||
constructor() {
|
||||
this.gatewayConnection = new DiscordClient(DISCORD_TOKEN, {
|
||||
intents: 0 | (1 << 0) | (1 << 9) | (1 << 15) // GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT
|
||||
});
|
||||
this.process = null;
|
||||
this.rconConnection = null;
|
||||
this.playerCount = 0;
|
||||
this.serverStartedAt = null;
|
||||
this.hasSentHelloMessage = false;
|
||||
this.players = [];
|
||||
|
||||
this.gatewayConnection.on("MESSAGE_CREATE", (e) => {
|
||||
if (
|
||||
e.channel_id === TARGET_CHANNEL_ID &&
|
||||
e.guild_id === TARGET_GUILD_ID &&
|
||||
!e.webhook_id
|
||||
) {
|
||||
if (e.content === "!mstart") {
|
||||
this.userRequestedServerJob();
|
||||
} else if (e.content === "!mstatus") {
|
||||
const message = `:scroll: **Server Information**\n` +
|
||||
`Player Count: **${this.playerCount}**\n` +
|
||||
`${this.serverStartedAt ? `Started: <t:${Math.floor(this.serverStartedAt/1000)}:R>` : "Started: [server is closed]"}\n` +
|
||||
`\n` +
|
||||
`:busts_in_silhouette: **Players** (${this.players.length})\n` +
|
||||
this.players.length > 0 ? this.players.map(p => `${p}\n`) : "[no players]\n" +
|
||||
`:gear: **Runtime Information**\n` +
|
||||
`process exists: \`${!!this.process}\`\n` +
|
||||
`gatewayConnection.user exists: \`${!!this.gatewayConnection.user}\``;
|
||||
|
||||
this.sendExternalMessage(message);
|
||||
} else {
|
||||
this.sendMinecraftMessage(
|
||||
e.author.username,
|
||||
e.content,
|
||||
e.attachments,
|
||||
e.referenced_message
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.gatewayConnection.on("READY", () => {
|
||||
if (!this.hasSentHelloMessage) {
|
||||
this.hasSentHelloMessage = true;
|
||||
this.sendExternalMessage(":bridge_at_night: The bridge server has started! Type `!mstart` in the chat to start the Minecraft server. Any further updates related to the server's status will be sent in this channel.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
executeMinecraftCommand(cmd) {
|
||||
if (!this.process) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.process.stdin.write(`${cmd}\n`);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.gatewayConnection.connect();
|
||||
}
|
||||
|
||||
spawnProcess() {
|
||||
log("server job: spawnProcess(): spawning");
|
||||
|
||||
if (this.process) {
|
||||
log("server job: spawnProcess(): another instance already exists");
|
||||
this.sendExternalMessage(":thinking: It seems like the server is already running! If you think this is a mistake, contact the server manager.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.process = createServerChildProcess();
|
||||
process.stdin.pipe(this.process.stdin);
|
||||
|
||||
this.process.stderr.on("data", async (rawDataBuffer) => {
|
||||
const stringData = rawDataBuffer.toString().trim();
|
||||
logError(`[server process stderr]: ${stringData}`);
|
||||
});
|
||||
|
||||
this.process.stdout.on("data", async (rawDataBuffer) => {
|
||||
const stringData = rawDataBuffer.toString().trim();
|
||||
log(`[server process stdout]: ${stringData}`);
|
||||
|
||||
const joinResult = joinNotificationRegex.exec(stringData);
|
||||
if (joinResult) {
|
||||
this.playerCountUpdate(1);
|
||||
this.players.push(joinResult.groups.username);
|
||||
await this.sendExternalMessage(`:door: **${joinResult.groups.username}** joined the game. Player count is **${this.playerCount}**.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const leaveResult = leaveNotificationRegex.exec(stringData);
|
||||
if (leaveResult) {
|
||||
this.playerCountUpdate(-1);
|
||||
const existingPlayerIndex = this.players.indexOf(leaveResult.groups.username);
|
||||
if (existingPlayerIndex !== -1) {
|
||||
this.players.splice(existingPlayerIndex, 1);
|
||||
}
|
||||
await this.sendExternalMessage(`:door: **${leaveResult.groups.username}** left the game. Player count is **${this.playerCount}**.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageResult = chatMessageRegex.exec(stringData);
|
||||
if (messageResult) {
|
||||
await this.sendExternalMessage(messageResult.groups.message, messageResult.groups.username);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
this.process.on("spawn", () => {
|
||||
log("server process: spawn");
|
||||
this.serverStartedAt = performance.now();
|
||||
this.process.stdout.resume();
|
||||
this.process.stderr.resume();
|
||||
this.sendExternalMessage(":zap: Server started. It might take some time before it fully initializes.");
|
||||
});
|
||||
|
||||
this.process.on("exit", (code) => {
|
||||
log(`server process: exited with code ${code}`);
|
||||
this.process = null;
|
||||
this.serverStartedAt = null;
|
||||
this.sendExternalMessage(":zap: Server is now closed.");
|
||||
});
|
||||
|
||||
this.process.on("error", (e) => {
|
||||
logError("server process: error", e);
|
||||
this.process = null;
|
||||
this.serverStartedAt = null;
|
||||
this.sendExternalMessage(":flushed: Server process error.");
|
||||
});
|
||||
}
|
||||
|
||||
stopProcess() {
|
||||
if (this.process) {
|
||||
log("server job: closing process...");
|
||||
this.process.kill("SIGINT");
|
||||
} else {
|
||||
log("server job: no process to kill");
|
||||
}
|
||||
}
|
||||
|
||||
async sendExternalMessage(content, username=null, avatarURL=null) {
|
||||
// try to generate an avatar for a specific username
|
||||
if (username && !avatarURL) {
|
||||
avatarURL = `https://avatars.dicebear.com/api/identicon/${username.substring(0, 2)}.jpg`;
|
||||
}
|
||||
return await fetch(WEBHOOK_URL, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
username,
|
||||
avatar_url: avatarURL,
|
||||
allowed_mentions: {
|
||||
parse: ["users"],
|
||||
users: []
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sendMinecraftMessage(username, content, attachments=[], referencedMessage=null) {
|
||||
const tellrawPayload = [
|
||||
{ text: "[" },
|
||||
{ text: `${username}`, color: "gray" },
|
||||
{ text: "]" },
|
||||
{ text: " " },
|
||||
];
|
||||
|
||||
if (referencedMessage) {
|
||||
let trimmedContent = referencedMessage.content.substring(0, 70);
|
||||
if (trimmedContent !== referencedMessage.content) {
|
||||
trimmedContent += "...";
|
||||
}
|
||||
tellrawPayload.push({
|
||||
text: `<replying to ${referencedMessage.author.username}: ${trimmedContent}>`,
|
||||
color: "gray"
|
||||
});
|
||||
tellrawPayload.push({
|
||||
text: " "
|
||||
});
|
||||
}
|
||||
|
||||
attachments.forEach((e) => {
|
||||
tellrawPayload.push({
|
||||
text: `<open attachment: ${e.filename || "[unknown]"}>`,
|
||||
color: "gray",
|
||||
clickEvent: {
|
||||
action: "open_url",
|
||||
value: e.proxy_url
|
||||
}
|
||||
});
|
||||
tellrawPayload.push({
|
||||
text: " "
|
||||
});
|
||||
});
|
||||
|
||||
tellrawPayload.push({ text: content });
|
||||
|
||||
if (this.playerCount > 0) {
|
||||
this.executeMinecraftCommand(`tellraw @a ${JSON.stringify(tellrawPayload)}`);
|
||||
}
|
||||
}
|
||||
|
||||
playerCountUpdate(amount) {
|
||||
this.playerCount += amount;
|
||||
|
||||
if (this.playerCount === 0) {
|
||||
log("server job: playerCount has reached 0, will close child server process");
|
||||
this.sendExternalMessage(":wave: Everyone has left the server. Closing...");
|
||||
this.stopProcess();
|
||||
}
|
||||
}
|
||||
|
||||
userRequestedServerJob() {
|
||||
log("server job: user requested server job, spawning...");
|
||||
this.spawnProcess();
|
||||
|
||||
// wait for 5 minutes - if no one has joined the server in that time, close it
|
||||
setTimeout(() => {
|
||||
if (this.playerCount === 0) {
|
||||
log("server job: no players on server after 5 minutes, closing...");
|
||||
this.sendExternalMessage(":pensive: Server was started, yet no one joined after 5 minutes. Closing again...");
|
||||
this.stopProcess();
|
||||
}
|
||||
}, 300000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function main() {
|
||||
for (const [name, value] of Object.entries(requiredEnv)) {
|
||||
if (value === undefined) {
|
||||
throw new Error(`Required env variable ${name} was not found`);
|
||||
}
|
||||
}
|
||||
|
||||
const bridge = new Bridge();
|
||||
bridge.start();
|
||||
|
||||
["exit", "SIGINT", "SIGUSR1", "SIGUSR2", "uncaughtException", "SIGTERM"].forEach((eventType) => {
|
||||
process.on(eventType, () => {
|
||||
bridge.stopProcess();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
main();
|
11
package.json
Normal file
11
package.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "minecraft-bridge",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"node-fetch": "^3.2.6",
|
||||
"ws": "^8.8.0"
|
||||
}
|
||||
}
|
47
yarn.lock
Normal file
47
yarn.lock
Normal file
|
@ -0,0 +1,47 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
data-uri-to-buffer@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
|
||||
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
|
||||
|
||||
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
|
||||
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
|
||||
dependencies:
|
||||
node-domexception "^1.0.0"
|
||||
web-streams-polyfill "^3.0.3"
|
||||
|
||||
formdata-polyfill@^4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
|
||||
dependencies:
|
||||
fetch-blob "^3.1.2"
|
||||
|
||||
node-domexception@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||
|
||||
node-fetch@^3.2.6:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.6.tgz#6d4627181697a9d9674aae0d61548e0d629b31b9"
|
||||
integrity sha512-LAy/HZnLADOVkVPubaxHDft29booGglPFDr2Hw0J1AercRh01UiVFm++KMDnJeH9sHgNB4hsXPii7Sgym/sTbw==
|
||||
dependencies:
|
||||
data-uri-to-buffer "^4.0.0"
|
||||
fetch-blob "^3.1.4"
|
||||
formdata-polyfill "^4.0.10"
|
||||
|
||||
web-streams-polyfill@^3.0.3:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
|
||||
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
|
||||
|
||||
ws@^8.8.0:
|
||||
version "8.8.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.0.tgz#8e71c75e2f6348dbf8d78005107297056cb77769"
|
||||
integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==
|
Loading…
Reference in a new issue