first commit

This commit is contained in:
Soph :3 2024-07-01 15:39:17 +03:00
commit 95789a6931
Signed by: sophie
GPG key ID: EDA5D222A0C270F2
8 changed files with 611 additions and 0 deletions

176
.gitignore vendored Normal file
View file

@ -0,0 +1,176 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
config.json

17
README.md Normal file
View file

@ -0,0 +1,17 @@
# autowhitelist
This ts project automatically whitelists people to a minecraft server from a discord. It's very very simple.
Supports multiple users in one message, e.g "USERONE", "USERTWO". This isn't configurable, but quite easy to remove.
All whitelists can also be removed by just deleting the message, helping both users incase username changes or other issues, and staff.
Run `bun i`, copy `config.example.json` to `config.json` and change all of the settings. The channel option is the channel ID, which you can copy by using developer mode on discord. Then, just start the bot.
If you get something as such from the bot, then everything's done right:
```
RCON connected.
RCON authenicated.
Ready! Logged in as Slop Bridge#2702
```
There is also an option to get rid of nicknames changes. Set "changeNicknames" to "false".
This project was created using `bun init` in bun v1.1.13. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

BIN
bun.lockb Executable file

Binary file not shown.

10
config.example.json Normal file
View file

@ -0,0 +1,10 @@
{
"token": "discord bot token",
"rcon": {
"port": 25567,
"host": "my.server.com",
"password": "very secure password"
},
"channel": "channel id for my igns channel",
"changeNicknames": true
}

128
index.ts Normal file
View file

@ -0,0 +1,128 @@
import { Client, Events, GatewayIntentBits, TextChannel } from "discord.js";
import { Rcon } from "./rcon";
import config from "./config.json" assert { type: "json" };
const rcon = new Rcon(config.rcon.host, config.rcon.port, config.rcon.password);
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
rcon.on("connect", () => {
console.log("RCON connected.");
});
rcon.on("auth", () => {
console.log("RCON authenicated.");
});
client.once(Events.ClientReady, async (readyClient) => {
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
let playerUsernames: string[] = [];
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"));
if (!f[1].reactions.resolve("✅")) await f[1].react("✅");
let member = f[1].member;
if (!member) member = await f[1].guild.members.fetch(f[1].author.id);
if (config.changeNicknames) {
if (member.nickname !== f[1].content.split("\n")[0]) {
try {
await member.setNickname(f[1].content.split("\n")[0], "whitelisted");
} catch {}
}
}
}
for (let i = 0; i < playerUsernames.length; i++) {
setTimeout(() => {
rcon.send("whitelist add " + playerUsernames[i]);
}, i * 20);
}
console.log("Mass-whitelisted.");
});
client.on(Events.MessageCreate, async (message) => {
if (
message.channelId == config.channel &&
message.author!.id != client.user!.id
) {
const channel = (await client.channels.fetch(
config.channel
)) as TextChannel;
const messages = await channel.messages.fetch();
if (messages.filter((z) => z.author.id == message.author.id).size > 1) {
await message.delete();
const deleteThisMessage = await channel.send(
`<@${message.author.id}>, you can only have one message in this channel.`
);
setTimeout(async () => {
await deleteThisMessage.delete();
}, 5000);
return;
}
const playerUsernames = message.content.split("\n");
let member = message.member;
if (!member) member = await message.guild!.members.fetch(message.author.id);
if (config.changeNicknames) {
try {
await member.setNickname(playerUsernames[0]);
} catch {}
}
for (let i = 0; i < playerUsernames.length; i++) {
setTimeout(() => {
rcon.send("whitelist add " + playerUsernames[i]);
}, i * 20);
}
await message.react("✅");
}
});
client.on(Events.MessageDelete, async (message) => {
if (
message.channelId == config.channel &&
message.author!.id != client.user!.id
) {
if (message.reactions.resolve("✅")) {
const deleteThisMessage = await message.channel.send(
`<@${message.author?.id}>, you are no longer whitelisted.`
);
const member = await message.guild!.members.fetch(message.author!.id);
if (config.changeNicknames) {
try {
await member.setNickname(null, "unwhitelisted");
} catch {}
setTimeout(async () => {
await deleteThisMessage.delete();
}, 5000);
}
const usernames = message.content?.split("\n") || [];
for (let i = 0; i < usernames.length; i++) {
setTimeout(() => {
rcon.send("whitelist remove " + usernames[i]);
}, i * 20);
}
}
}
});
client.login(config.token);
rcon.connect();

18
package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "autowhitelist",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest",
"typed-emitter": "^2.1.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"bufferutil": "^4.0.8",
"discord.js": "^14.15.3",
"utf-8-validate": "^6.0.4",
"zlib-sync": "^0.1.9"
}
}

235
rcon.ts Normal file
View file

@ -0,0 +1,235 @@
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<Events>) {
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);
})
.on("connect", () => {
this.socketOnConnect();
})
.on("error", (err) => {
this.emit("error", err);
})
.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;
};
}

27
tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}