diff --git a/bun.lockb b/bun.lockb index 2f27a09..78c1472 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.ts b/index.ts index 85f9368..faa9cba 100644 --- a/index.ts +++ b/index.ts @@ -1,9 +1,14 @@ -import { Client, Events, GatewayIntentBits, TextChannel } from "discord.js"; -import { Rcon } from "./rcon"; +import { Client, Events, GatewayIntentBits, Message, TextChannel } from "discord.js"; +import { Rcon } from "rconts"; import config from "./config.json" assert { type: "json" }; -const rcon = new Rcon(config.rcon.host, config.rcon.port, config.rcon.password); -let rconReconnectTimeout: Timer | null | undefined; +const rcon = new Rcon({ + host: config.rcon.host, + port: config.rcon.port, + password: config.rcon.password +}); +let ignoreDeletion: Set = new Set(); + const client = new Client({ intents: [ @@ -13,33 +18,56 @@ const client = new Client({ ], }); -rcon.on("connect", () => { +rcon.on("connected", () => { console.log("RCON connected."); }); -rcon.on("auth", () => { - console.log("RCON authenicated."); -}); +async function whitelist(message: Message, username: string) { + const response = await rcon.exec("whitelist add " + username); + let deleteThisMessage; -rcon.on("end", () => { - console.log("RCON ended. Reconnecting in 2000ms.") - if(rconReconnectTimeout) { - clearTimeout(rconReconnectTimeout); + if (response == "That player does not exist") { + try { + ignoreDeletion.add(message.id); + await message.delete(); + + } catch { + + } + deleteThisMessage = await message.channel.send( + `<@${message.author.id}>, you have not been whitelisted. Please join the server at least once, and then resend your IGN.` + ); + } else if (response.includes("Incorrect argument for")) { + try { + ignoreDeletion.add(message.id); + await message.delete(); + + } catch { + + } + + deleteThisMessage = await message.channel.send( + `<@${message.author.id}>, your message was invalid. Include ONLY your username, nothing else.` + ); } - rconReconnectTimeout = setTimeout(() => { - rcon.connect() - }, 2000) -}) + if(deleteThisMessage) { + setTimeout(async () => { + await deleteThisMessage.delete(); + }, 5000); + } + console.log("[whitelist] response for", username, "is", response); +} + client.once(Events.ClientReady, async (readyClient) => { console.log(`Ready! Logged in as ${readyClient.user.tag}`); - let playerUsernames: string[] = []; + let playerUsernames: Map = new Map(); const channel = (await client.channels.fetch(config.channel)) as TextChannel; const messages = await channel.messages.fetch(); for await (const f of messages) { - playerUsernames = playerUsernames.concat(f[1].content.split("\n")); + playerUsernames.set(f[1], f[1].content.split("\n")); if (!f[1].reactions.resolve("✅")) await f[1].react("✅"); let member = f[1].member; try { @@ -51,21 +79,22 @@ client.once(Events.ClientReady, async (readyClient) => { if (member.nickname !== f[1].content.split("\n")[0]) { try { await member.setNickname(f[1].content.split("\n")[0], "whitelisted"); - } catch {} + } catch { } } } } - for (let i = 0; i < playerUsernames.length; i++) { - setTimeout(() => { - rcon.send("whitelist add " + playerUsernames[i]); - }, i * 20); + for await (let u of playerUsernames) { + for await(let name of u[1]) { + await whitelist(u[0], name); + } } console.log("Mass-whitelisted."); }); client.on(Events.MessageCreate, async (message) => { + if ( message.channelId == config.channel && message.author!.id != client.user!.id @@ -96,19 +125,24 @@ client.on(Events.MessageCreate, async (message) => { if (config.changeNicknames) { try { await member.setNickname(playerUsernames[0]); - } catch {} + } catch { } } - for (let i = 0; i < playerUsernames.length; i++) { - setTimeout(() => { - rcon.send("whitelist add " + playerUsernames[i]); - }, i * 20); + for await (let name of playerUsernames) { + await whitelist(message, name) } - await message.react("✅"); + try { + await message.react("✅"); + } catch { + } } }); client.on(Events.MessageDelete, async (message) => { + if (ignoreDeletion.has(message.id)) { + ignoreDeletion.delete(message.id); + return; + } if ( message.channelId == config.channel && message.author!.id != client.user!.id @@ -122,17 +156,17 @@ client.on(Events.MessageDelete, async (message) => { if (config.changeNicknames) { try { await member.setNickname(null, "unwhitelisted"); - } catch {} - setTimeout(async () => { - await deleteThisMessage.delete(); - }, 5000); - } - const usernames = message.content?.split("\n") || []; + } catch { } - for (let i = 0; i < usernames.length; i++) { - setTimeout(() => { - rcon.send("whitelist remove " + usernames[i]); - }, i * 20); + } + setTimeout(async () => { + await deleteThisMessage.delete(); + }, 5000); + const playerUsernames = message.content?.split("\n") || []; + + for await (let name of playerUsernames) { + const response = await rcon.exec("whitelist remove " + name); + console.log("[delete-whitelist] response for", name, "is", response); } } } diff --git a/package.json b/package.json index 6499df5..83f63d8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "dependencies": { "bufferutil": "^4.0.8", "discord.js": "^14.15.3", + "node-gyp": "^11.1.0", + "rconts": "git+https://git.sad.ovh/sophie/rconts.git#8f6d5a48330a35f7e4e288b5a3f0104e249aa575", "utf-8-validate": "^6.0.4", "zlib-sync": "^0.1.9" } diff --git a/rcon.ts b/rcon.ts deleted file mode 100644 index e30dfb3..0000000 --- a/rcon.ts +++ /dev/null @@ -1,235 +0,0 @@ -import EventEmitter from "events"; -import * as net from "net"; -import * as dgram from "dgram"; -import { Buffer } from "buffer"; -import type TypedEmitter from "typed-emitter"; - -type Events = { - error: (error: Error) => void; - auth: () => void; - response: (response: string) => void; - connect: () => void; - end: () => void; - done: () => void; -}; - -export const PacketType = { - COMMAND: 0x02, - AUTH: 0x03, - RESPONSE_VALUE: 0x00, - RESPONSE_AUTH: 0x02, -}; - -interface Options { - tcp?: boolean; - challenge?: boolean; - id?: number; -} -export class Rcon extends (EventEmitter as new () => TypedEmitter) { - private host: string; - private port: number; - private password: string; - private rconId: number; - private hasAuthed: boolean; - private outstandingData: Uint8Array | null; - private tcp: boolean; - private challenge: boolean; - private _challengeToken: string; - private _tcpSocket!: net.Socket; - private _udpSocket!: dgram.Socket; - - constructor(host: string, port: number, password: string, options?: Options) { - super(); - options = options || {}; - this.host = host; - this.port = port; - this.password = password; - this.rconId = options.id || 0x0012d4a6; // This is arbitrary in most cases - this.hasAuthed = false; - this.outstandingData = null; - this.tcp = options.tcp ? options.tcp : true; - this.challenge = options.challenge ? options.challenge : true; - this._challengeToken = ""; - } - - public send = (data: string, cmd?: number, id?: number): void => { - let sendBuf: Buffer; - if (this.tcp) { - cmd = cmd || PacketType.COMMAND; - id = id || this.rconId; - - const length = Buffer.byteLength(data); - sendBuf = Buffer.alloc(length + 14); - sendBuf.writeInt32LE(length + 10, 0); - sendBuf.writeInt32LE(id, 4); - sendBuf.writeInt32LE(cmd, 8); - sendBuf.write(data, 12); - sendBuf.writeInt16LE(0, length + 12); - } else { - if (this.challenge && !this._challengeToken) { - this.emit("error", new Error("Not authenticated")); - return; - } - let str = "rcon "; - if (this._challengeToken) str += this._challengeToken + " "; - if (this.password) str += this.password + " "; - str += data + "\n"; - sendBuf = Buffer.alloc(4 + Buffer.byteLength(str)); - sendBuf.writeInt32LE(-1, 0); - sendBuf.write(str, 4); - } - this._sendSocket(sendBuf); - }; - - private _sendSocket = (buf: Buffer) => { - if (this._tcpSocket) { - this._tcpSocket.write(buf.toString("binary"), "binary"); - } else if (this._udpSocket) { - this._udpSocket.send(buf, 0, buf.length, this.port, this.host); - } - }; - - public connect = (): void => { - if (this.tcp) { - this._tcpSocket = net.createConnection(this.port, this.host); - this._tcpSocket.on("data", (data) => { - this._tcpSocketOnData(data); - }); - this._tcpSocket.on("connect", () => { - this.socketOnConnect(); - }); - this._tcpSocket.on("error", (err) => { - //this.emit("error", err); - this.socketOnEnd() - }); - this._tcpSocket.on("end", () => { - this.socketOnEnd(); - }); - } else { - this._udpSocket = dgram.createSocket("udp4"); - this._udpSocket - .on("message", (data) => { - this._udpSocketOnData(data); - }) - .on("listening", () => { - this.socketOnConnect(); - }) - .on("error", (err) => { - this.emit("error", err); - }) - .on("close", () => { - this.socketOnEnd(); - }); - this._udpSocket.bind(0); - } - }; - - public disconnect = (): void => { - if (this._tcpSocket) this._tcpSocket.end(); - if (this._udpSocket) this._udpSocket.close(); - }; - - public setTimeout = (timeout: number, callback: () => void): void => { - if (!this._tcpSocket) return; - this._tcpSocket.setTimeout(timeout, () => { - this._tcpSocket.end(); - if (callback) callback(); - }); - }; - - private _udpSocketOnData = (data: Buffer) => { - const a = data.readUInt32LE(0); - if (a === 0xffffffff) { - const str = data.toString("utf-8", 4); - const tokens = str.split(" "); - if ( - tokens.length === 3 && - tokens[0] === "challenge" && - tokens[1] === "rcon" - ) { - this._challengeToken = tokens[2] - .substring(0, tokens[2].length - 1) - .trim(); - this.hasAuthed = true; - this.emit("auth"); - } else { - this.emit("response", str.substring(1, str.length - 2)); - } - } else { - this.emit("error", new Error("Received malformed packet")); - } - }; - - private _tcpSocketOnData = (data: Buffer) => { - if (this.outstandingData != null) { - data = Buffer.concat( - [this.outstandingData, data], - this.outstandingData.length + data.length - ); - this.outstandingData = null; - } - - while (data.length) { - const len = data.readInt32LE(0); - if (!len) return; - - const id = data.readInt32LE(4); - const type = data.readInt32LE(8); - - if (len >= 10 && data.length >= len + 4) { - if (id === this.rconId) { - if (!this.hasAuthed && type === PacketType.RESPONSE_AUTH) { - this.hasAuthed = true; - this.emit("auth"); - } else if (type === PacketType.RESPONSE_VALUE) { - // Read just the body of the packet (truncate the last null byte) - // See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol for details - let str = data.toString("utf8", 12, 12 + len - 10); - - if (str.charAt(str.length - 1) === "\n") { - // Emit the response without the newline. - str = str.substring(0, str.length - 1); - } - - this.emit("response", str); - } - } else { - this.emit("error", new Error("Authentication failed")); - } - - data = data.slice(12 + len - 8); - } else { - // Keep a reference to the chunk if it doesn't represent a full packet - this.outstandingData = data; - break; - } - } - }; - - public socketOnConnect = (): void => { - this.emit("connect"); - - if (this.tcp) { - this.send(this.password, PacketType.AUTH); - } else if (this.challenge) { - const str = "challenge rcon\n"; - const sendBuf = Buffer.alloc(str.length + 4); - sendBuf.writeInt32LE(-1, 0); - sendBuf.write(str, 4); - this._sendSocket(sendBuf); - } else { - const sendBuf = Buffer.alloc(5); - sendBuf.writeInt32LE(-1, 0); - sendBuf.writeUInt8(0, 4); - this._sendSocket(sendBuf); - - this.hasAuthed = true; - this.emit("auth"); - } - }; - - public socketOnEnd = (): void => { - this.emit("end"); - this.hasAuthed = false; - }; -}