2022-02-11 15:20:59 +02:00
// 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
2022-02-11 15:20:59 +02:00
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
} ;
2022-02-15 17:07:03 +02:00
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/ ;
2022-02-11 15:20:59 +02:00
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 ;
2022-02-15 14:25:26 +02:00
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" ) ;
}
}
2022-02-11 15:20:59 +02:00
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" , ( ) => {
2022-02-15 13:41:30 +02:00
console . log ( "gateway: closed, reconnecting in 4000ms" ) ;
2022-02-11 15:20:59 +02:00
setTimeout ( ( ) => {
console . log ( "gateway: reconnecting" ) ;
this . connect ( token ) ;
} , 4000 ) ;
} ) ;
2022-02-15 14:25:26 +02:00
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 ) ;
} ) ;
2022-02-11 15:20:59 +02:00
}
_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
}
} ) ;
}
2022-02-16 14:02:37 +02:00
async function sendMinecraftMessageAs ( rcon , username , content , attachments = [ ] , referencedMessage = null ) {
const tellrawPayload = [
2022-02-11 15:20:59 +02:00
{ text : "[" } ,
{ text : ` ${ username } ` , color : "gray" } ,
{ text : "]" } ,
{ text : " " } ,
2022-02-16 14:02:37 +02:00
] ;
if ( referencedMessage ) {
let trimmedContent = referencedMessage . content . substring ( 0 , 50 ) ;
if ( trimmedContent !== referencedMessage . content ) {
trimmedContent += "..." ;
}
tellrawPayload . push ( {
text : ` <replying to ${ referencedMessage . author . username } : ${ trimmedContent } > `
} ) ;
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 } ) ;
rcon . send ( ` tellraw @a ${ JSON . stringify ( tellrawPayload ) } ` ) ;
2022-02-11 15:20:59 +02:00
}
async function main ( ) {
rconConnection . on ( "error" , ( e ) => {
console . error ( "rcon: got error" , e ) ;
if ( ! rconConnection . hasAuthed ) {
2022-02-15 13:41:30 +02:00
console . log ( "rcon: reconnecting in 5000ms due to error before hasAuthed (server might not be up yet?)" ) ;
2022-02-11 15:20:59 +02:00
setTimeout ( ( ) => {
rconConnection . connect ( ) ;
2022-02-15 13:41:30 +02:00
} , 5000 ) ;
2022-02-11 15:20:59 +02:00
}
} ) ;
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 ) {
2022-02-16 14:02:37 +02:00
sendMinecraftMessageAs ( rconConnection , e . message . author . username , e . message . content , e . message . attachments , e . message . referenced _message ) ;
2022-02-11 15:20:59 +02:00
}
} ;
rconConnection . connect ( ) ;
gateway . connect ( TOKEN ) ;
process . stdin . resume ( ) ;
process . stdin . on ( "data" , async ( rawDataBuffer ) => {
const stringData = rawDataBuffer . toString ( ) . trim ( ) ;
console . log ( stringData ) ;
2022-02-15 13:41:30 +02:00
const joinResult = joinNotificationRegex . exec ( stringData ) ;
if ( joinResult ) {
await sendBridgeMessageAs ( TARGET _GUILD _ID , TARGET _CHANNEL _ID , ` ** ${ joinResult . groups . username } ** joined the game ` , null , null ) ;
2022-02-11 15:20:59 +02:00
return ;
2022-02-15 13:41:30 +02:00
}
2022-02-11 15:20:59 +02:00
2022-02-15 13:41:30 +02:00
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 ;
}
2022-02-11 15:20:59 +02:00
} ) ;
}
await main ( ) ;