Create docker build config
This commit is contained in:
parent
7b124b1d83
commit
78fc178652
|
@ -0,0 +1,13 @@
|
||||||
|
node_modules
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
.vscode
|
||||||
|
.env.template
|
||||||
|
.gitmodules
|
||||||
|
.prettierrc
|
||||||
|
.eslintrc.js
|
|
@ -0,0 +1,34 @@
|
||||||
|
FROM oven/bun:latest AS base
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
FROM base AS install
|
||||||
|
RUN mkdir -p /temp/dev
|
||||||
|
COPY package.json bun.lockb /temp/dev/
|
||||||
|
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
RUN mkdir -p /temp/prod
|
||||||
|
COPY package.json bun.lockb /temp/prod
|
||||||
|
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||||
|
|
||||||
|
FROM base AS prerelease
|
||||||
|
COPY --from=install /temp/dev/node_modules node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
#RUN bun test
|
||||||
|
#RUN bun build
|
||||||
|
|
||||||
|
FROM base AS release
|
||||||
|
COPY --from=install /temp/prod/node_modules node_modules
|
||||||
|
COPY --from=prerelease /usr/src/app/src/ ./src
|
||||||
|
COPY --from=prerelease /usr/src/app/package.json .
|
||||||
|
COPY --from=prerelease /usr/src/app/config ./config
|
||||||
|
COPY --from=prerelease /usr/src/app/public ./public
|
||||||
|
COPY --from=prerelease /usr/src/app/mppkey ./mppkey
|
||||||
|
COPY --from=prerelease /usr/src/app/tsconfig.json .
|
||||||
|
COPY --from=prerelease /usr/src/app/prisma ./prisma
|
||||||
|
COPY --from=prerelease /usr/src/app/.env .
|
||||||
|
|
||||||
|
USER bun
|
||||||
|
EXPOSE 8443/tcp
|
||||||
|
ENTRYPOINT [ "bun", "." ]
|
|
@ -66,9 +66,6 @@ This has always been the future intention of this project.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- Fully implement and test tags
|
|
||||||
- Tags are sent to clients now
|
|
||||||
- Check if tags are sent to everyone
|
|
||||||
- Channel data saving
|
- Channel data saving
|
||||||
- Permission groups and permissions
|
- Permission groups and permissions
|
||||||
- Probable permission groups: owner, admin, mod, trialmod, default
|
- Probable permission groups: owner, admin, mod, trialmod, default
|
||||||
|
@ -104,6 +101,12 @@ This has always been the future intention of this project.
|
||||||
- Check for different messages?
|
- Check for different messages?
|
||||||
- Check for URL?
|
- Check for URL?
|
||||||
- Notifications for server-generated XSS?
|
- Notifications for server-generated XSS?
|
||||||
|
- Migrate to PostgreSQL instead of SQLite
|
||||||
|
- Likely a low priority, we use prisma anyway, but it would be nice to have a server
|
||||||
|
- Implement user caching
|
||||||
|
- Skip redis due to the infamous licensing issues
|
||||||
|
- Probably use a simple in-memory cache
|
||||||
|
- Likely store with leveldb or JSON
|
||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
# Channel config file
|
# Channel config file
|
||||||
|
|
||||||
|
# Which channels to keep loaded on startup
|
||||||
forceLoad:
|
forceLoad:
|
||||||
- lobby
|
- lobby
|
||||||
- test/awkward
|
- test/awkward
|
||||||
|
|
||||||
|
# Default settings for lobbies
|
||||||
lobbySettings:
|
lobbySettings:
|
||||||
lobby: true
|
lobby: true
|
||||||
chat: true
|
chat: true
|
||||||
|
@ -10,19 +13,35 @@ lobbySettings:
|
||||||
visible: true
|
visible: true
|
||||||
color: "#73b3cc"
|
color: "#73b3cc"
|
||||||
color2: "#273546"
|
color2: "#273546"
|
||||||
|
|
||||||
|
# Default settings for regular channels
|
||||||
defaultSettings:
|
defaultSettings:
|
||||||
chat: true
|
chat: true
|
||||||
crownsolo: false
|
crownsolo: false
|
||||||
color: "#3b5054"
|
color: "#3b5054"
|
||||||
color2: "#001014"
|
color2: "#001014"
|
||||||
visible: true
|
visible: true
|
||||||
|
|
||||||
|
# Regexes to match against channel names to determine whether they are lobbies or not
|
||||||
|
# This doesn't affect the `isRealLobby` function, which is used to determine "classic" lobbies
|
||||||
lobbyRegexes:
|
lobbyRegexes:
|
||||||
- ^lobby[0-9][0-9]$
|
- ^lobby[0-9][0-9]$
|
||||||
- ^lobby[0-9]$
|
- ^lobby[0-9]$
|
||||||
- ^lobby$
|
- ^lobby$
|
||||||
- ^lobbyNaN$
|
- ^lobbyNaN$
|
||||||
- ^test/.+$
|
- ^test/.+$
|
||||||
|
|
||||||
|
# Backdoor channel ID for bypassing the lobby limit
|
||||||
lobbyBackdoor: lolwutsecretlobbybackdoor
|
lobbyBackdoor: lolwutsecretlobbybackdoor
|
||||||
|
|
||||||
|
# Channel ID for where you get sent when you join a channel that is full/you get banned/etc
|
||||||
fullChannel: test/awkward
|
fullChannel: test/awkward
|
||||||
|
|
||||||
|
# Whether to send the channel limit to the client
|
||||||
sendLimit: false
|
sendLimit: false
|
||||||
|
|
||||||
|
# Whether to give the crown to the user who had it when they rejoin
|
||||||
chownOnRejoin: true
|
chownOnRejoin: true
|
||||||
|
|
||||||
|
# Time in milliseconds to wait before destroying an empty channel
|
||||||
|
channelDestroyTimeout: 1000
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: prometheus
|
||||||
|
scrape_interval: "5s"
|
||||||
|
static_configs:
|
||||||
|
- targets: ["localhost:9090"]
|
||||||
|
|
||||||
|
- job_name: mpp
|
||||||
|
static_configs:
|
||||||
|
- targets: ["192.168.1.24:9100"]
|
|
@ -37,14 +37,15 @@ enableAdminEval: true
|
||||||
# The token validation scheme. Valid values are "none", "jwt" and "uuid".
|
# The token validation scheme. Valid values are "none", "jwt" and "uuid".
|
||||||
# This server will still validate existing tokens generated with other schemes if not set to "none", mimicking MPP.net's server.
|
# This server will still validate existing tokens generated with other schemes if not set to "none", mimicking MPP.net's server.
|
||||||
# This is set to "none" by default because MPP.com does not have a token system.
|
# This is set to "none" by default because MPP.com does not have a token system.
|
||||||
tokenAuth: none
|
tokenAuth: jwt
|
||||||
|
|
||||||
# The browser challenge scheme. Valid options are "none", "obf" and "basic".
|
# The browser challenge scheme. Valid options are "none", "obf" and "basic".
|
||||||
# This is to change what is sent in the "b" message.
|
# This is to change what is sent in the "b" message.
|
||||||
# "none" will disable the browser challenge,
|
# "none" will disable the browser challenge,
|
||||||
# "obf" will sent an obfuscated function to the client,
|
# "obf" will sent an obfuscated function to the client,
|
||||||
# and "basic" will just send a simple function that expects a boolean.
|
# and "basic" will just send a simple function that expects a boolean.
|
||||||
browserChallenge: none
|
# FIXME Note that "obf" is not implemented yet, and has undefined behavior.
|
||||||
|
browserChallenge: basic
|
||||||
|
|
||||||
# Scheme for generating user IDs.
|
# Scheme for generating user IDs.
|
||||||
# Valid options are "random", "sha256", "mpp" and "uuid".
|
# Valid options are "random", "sha256", "mpp" and "uuid".
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
enableLogFiles: true
|
|
@ -6,6 +6,10 @@
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Hri7566",
|
"author": "Hri7566",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"scripts": {
|
||||||
|
"start": "bun run src/index.ts",
|
||||||
|
"dev": "bun run src/index.ts --watch"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "5.17.0",
|
"@prisma/client": "5.17.0",
|
||||||
"@t3-oss/env-core": "^0.6.1",
|
"@t3-oss/env-core": "^0.6.1",
|
||||||
|
@ -17,6 +21,7 @@
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"keccak": "^2.1.0",
|
"keccak": "^2.1.0",
|
||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
|
"prom-client": "^15.1.3",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"yaml": "^2.5.0",
|
"yaml": "^2.5.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|
|
@ -68,7 +68,7 @@ export class Channel extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async save() {
|
private async save() {
|
||||||
this.logger.debug("Saving channel data");
|
//this.logger.debug("Saving channel data");
|
||||||
try {
|
try {
|
||||||
const info = this.getInfo();
|
const info = this.getInfo();
|
||||||
|
|
||||||
|
@ -78,16 +78,16 @@ export class Channel extends EventEmitter {
|
||||||
flags: JSON.stringify(this.flags)
|
flags: JSON.stringify(this.flags)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.debug("Channel data to save:", data);
|
//this.logger.debug("Channel data to save:", data);
|
||||||
|
|
||||||
await saveChannel(this.getID(), data);
|
await saveChannel(this.getID(), data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.debug("Error saving cannel:", err);
|
this.logger.warn("Error saving channel data:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async load() {
|
private async load() {
|
||||||
this.logger.debug("Loading saved data");
|
//this.logger.debug("Loading saved data");
|
||||||
try {
|
try {
|
||||||
const data = await getSavedChannel(this.getID());
|
const data = await getSavedChannel(this.getID());
|
||||||
if (data) {
|
if (data) {
|
||||||
|
@ -100,9 +100,11 @@ export class Channel extends EventEmitter {
|
||||||
forceloadChannel(this.getID());
|
forceloadChannel(this.getID());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug("Loaded channel data:", data);
|
//this.logger.debug("Loaded channel data:", data);
|
||||||
|
|
||||||
|
this.emit("update", this);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.debug("Error loading channel data:", err);
|
this.logger.error("Error loading channel data:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
|
@ -125,7 +127,7 @@ export class Channel extends EventEmitter {
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.logger = new Logger("Channel - " + _id);
|
this.logger = new Logger("Channel - " + _id, "logs/channel");
|
||||||
this.settings = {};
|
this.settings = {};
|
||||||
|
|
||||||
// Copy default settings
|
// Copy default settings
|
||||||
|
@ -209,7 +211,13 @@ export class Channel extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.ppl.length == 0 && !this.stays) {
|
if (this.ppl.length == 0 && !this.stays) {
|
||||||
|
if (config.channelDestroyTimeout) {
|
||||||
|
setTimeout(() => {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
|
}, config.channelDestroyTimeout);
|
||||||
|
} else {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -246,17 +254,21 @@ export class Channel extends EventEmitter {
|
||||||
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
|
.replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
const part = socket.getParticipant() as Participant;
|
||||||
|
|
||||||
let outgoing: ClientEvents["a"] = {
|
let outgoing: ClientEvents["a"] = {
|
||||||
m: "a",
|
m: "a",
|
||||||
a: msg.message,
|
a: msg.message,
|
||||||
t: Date.now(),
|
t: Date.now(),
|
||||||
p: socket.getParticipant() as Participant
|
p: part
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sendArray([outgoing]);
|
this.sendArray([outgoing]);
|
||||||
this.chatHistory.push(outgoing);
|
this.chatHistory.push(outgoing);
|
||||||
await saveChatHistory(this.getID(), this.chatHistory);
|
await saveChatHistory(this.getID(), this.chatHistory);
|
||||||
|
|
||||||
|
this.logger.info(`${part._id} ${part.name}: ${outgoing.a}`);
|
||||||
|
|
||||||
if (msg.message.startsWith("/")) {
|
if (msg.message.startsWith("/")) {
|
||||||
this.emit("command", msg, socket);
|
this.emit("command", msg, socket);
|
||||||
}
|
}
|
||||||
|
@ -373,6 +385,10 @@ export class Channel extends EventEmitter {
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.on("set owner id", id => {
|
||||||
|
this.setFlag("owner_id", id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -450,7 +466,7 @@ export class Channel extends EventEmitter {
|
||||||
|
|
||||||
// Set the verified settings
|
// Set the verified settings
|
||||||
for (const key of Object.keys(validatedSet)) {
|
for (const key of Object.keys(validatedSet)) {
|
||||||
this.logger.debug(`${key}: ${(validatedSet as any)[key]}`);
|
//this.logger.debug(`${key}: ${(validatedSet as any)[key]}`);
|
||||||
if ((validatedSet as any)[key] === false) continue;
|
if ((validatedSet as any)[key] === false) continue;
|
||||||
(this.settings as any)[key] = (set as any)[key];
|
(this.settings as any)[key] = (set as any)[key];
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface ChannelConfig {
|
||||||
fullChannel: string;
|
fullChannel: string;
|
||||||
sendLimit: boolean;
|
sendLimit: boolean;
|
||||||
chownOnRejoin: boolean;
|
chownOnRejoin: boolean;
|
||||||
|
channelDestroyTimeout: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = loadConfig<ChannelConfig>("config/channels.yml", {
|
export const config = loadConfig<ChannelConfig>("config/channels.yml", {
|
||||||
|
@ -34,5 +35,6 @@ export const config = loadConfig<ChannelConfig>("config/channels.yml", {
|
||||||
lobbyBackdoor: "lolwutsecretlobbybackdoor",
|
lobbyBackdoor: "lolwutsecretlobbybackdoor",
|
||||||
fullChannel: "test/awkward",
|
fullChannel: "test/awkward",
|
||||||
sendLimit: false,
|
sendLimit: false,
|
||||||
chownOnRejoin: true
|
chownOnRejoin: true,
|
||||||
|
channelDestroyTimeout: 1000
|
||||||
});
|
});
|
||||||
|
|
36
src/index.ts
36
src/index.ts
|
@ -1,9 +1,15 @@
|
||||||
/**
|
/**
|
||||||
* MPP Server 2
|
* MPP Server 2
|
||||||
* for mpp.dev
|
* for https://www.multiplayerpiano.dev/
|
||||||
* by Hri7566
|
* Written by Hri7566
|
||||||
|
* This code is licensed under the GNU General Public License v3.0.
|
||||||
|
* Please see `./LICENSE` for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for the server
|
||||||
|
**/
|
||||||
|
|
||||||
// There are a lot of unhinged bs comments in this repo
|
// There are a lot of unhinged bs comments in this repo
|
||||||
// Pay no attention to the ones that cuss you out
|
// Pay no attention to the ones that cuss you out
|
||||||
|
|
||||||
|
@ -11,15 +17,23 @@
|
||||||
import "./ws/server";
|
import "./ws/server";
|
||||||
import { loadForcedStartupChannels } from "./channel/forceLoad";
|
import { loadForcedStartupChannels } from "./channel/forceLoad";
|
||||||
import { Logger } from "./util/Logger";
|
import { Logger } from "./util/Logger";
|
||||||
|
import { startReadline } from "./util/readline";
|
||||||
|
import { startMetricsServer } from "./util/metrics";
|
||||||
|
|
||||||
// Let's construct an entire object just for one thing to be printed
|
// wrapper for some reason
|
||||||
// and then keep it in memory for the entirety of runtime
|
export function startServer() {
|
||||||
const logger = new Logger("Main");
|
// Let's construct an entire object just for one thing to be printed
|
||||||
logger.info("Forceloading startup channels...");
|
// and then keep it in memory for the entirety of runtime
|
||||||
loadForcedStartupChannels();
|
const logger = new Logger("Main");
|
||||||
|
logger.info("Forceloading startup channels...");
|
||||||
|
loadForcedStartupChannels();
|
||||||
|
|
||||||
// This literally breaks editors and they stick all the imports here instead of at the top
|
// Break the console
|
||||||
import "./util/readline";
|
startReadline();
|
||||||
|
|
||||||
// Nevermind we use it twice
|
// Nevermind, two things are printed
|
||||||
logger.info("Ready");
|
logger.info("Ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
|
startMetricsServer();
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { padNum, unimportant } from "./helpers";
|
import { padNum, unimportant } from "./helpers";
|
||||||
|
import { join } from "path";
|
||||||
|
import { existsSync, mkdirSync, appendFile, writeFile } from "fs";
|
||||||
|
import { config } from "./utilConfig";
|
||||||
|
|
||||||
export const logEvents = new EventEmitter();
|
export const logEvents = new EventEmitter();
|
||||||
|
|
||||||
|
const logFolder = "./logs";
|
||||||
|
// https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
|
||||||
|
const logRegex = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
||||||
|
|
||||||
|
if (config.enableLogFiles) {
|
||||||
|
if (!existsSync(logFolder)) mkdirSync(logFolder);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A logger that doesn't fuck with the readline prompt
|
* A logger that doesn't fuck with the readline prompt
|
||||||
* timestamps are likely wrong because of js timezones
|
* timestamps are likely wrong because of js timezones
|
||||||
|
@ -14,7 +25,7 @@ export class Logger {
|
||||||
* @param method The method from `console` to use
|
* @param method The method from `console` to use
|
||||||
* @param args The data to print
|
* @param args The data to print
|
||||||
**/
|
**/
|
||||||
private static log(method: string, ...args: any[]) {
|
private static log(method: string, logPath: string, ...args: any[]) {
|
||||||
// Un-fuck the readline prompt
|
// Un-fuck the readline prompt
|
||||||
process.stdout.write("\x1b[2K\r");
|
process.stdout.write("\x1b[2K\r");
|
||||||
|
|
||||||
|
@ -33,6 +44,23 @@ export class Logger {
|
||||||
|
|
||||||
// Emit the log event for remote consoles
|
// Emit the log event for remote consoles
|
||||||
logEvents.emit("log", method, unimportant(this.getDate()), unimportant(this.getHHMMSSMS()), args);
|
logEvents.emit("log", method, unimportant(this.getDate()), unimportant(this.getHHMMSSMS()), args);
|
||||||
|
|
||||||
|
if (config.enableLogFiles) {
|
||||||
|
// Write to file
|
||||||
|
(async () => {
|
||||||
|
const orig = unimportant(this.getDate()) + " " + unimportant(this.getHHMMSSMS()) + " " + args.join(" ") + "\n"
|
||||||
|
const text = orig.replace(logRegex, "");
|
||||||
|
if (!existsSync(logPath)) {
|
||||||
|
writeFile(logPath, text, (err) => {
|
||||||
|
if (err) console.error(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
appendFile(logPath, text, (err) => {
|
||||||
|
if (err) console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,14 +90,19 @@ export class Logger {
|
||||||
return new Date().toISOString().split("T")[0];
|
return new Date().toISOString().split("T")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(public id: string) { }
|
public logPath: string;
|
||||||
|
|
||||||
|
constructor(public id: string, logdir: string = logFolder) {
|
||||||
|
if (!existsSync(logdir)) mkdirSync(logdir);
|
||||||
|
this.logPath = join(logdir, `${encodeURIComponent(this.id)}.log`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Print an info message
|
* Print an info message
|
||||||
* @param args The data to print
|
* @param args The data to print
|
||||||
**/
|
**/
|
||||||
public info(...args: any[]) {
|
public info(...args: any[]) {
|
||||||
Logger.log("log", `[${this.id}]`, `\x1b[34m[info]\x1b[0m`, ...args);
|
Logger.log("log", this.logPath, `[${this.id}]`, `\x1b[34m[info]\x1b[0m`, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,7 +110,7 @@ export class Logger {
|
||||||
* @param args The data to print
|
* @param args The data to print
|
||||||
**/
|
**/
|
||||||
public error(...args: any[]) {
|
public error(...args: any[]) {
|
||||||
Logger.log("error", `[${this.id}]`, `\x1b[31m[error]\x1b[0m`, ...args);
|
Logger.log("error", this.logPath, `[${this.id}]`, `\x1b[31m[error]\x1b[0m`, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,7 +118,7 @@ export class Logger {
|
||||||
* @param args The data to print
|
* @param args The data to print
|
||||||
**/
|
**/
|
||||||
public warn(...args: any[]) {
|
public warn(...args: any[]) {
|
||||||
Logger.log("warn", `[${this.id}]`, `\x1b[33m[warn]\x1b[0m`, ...args);
|
Logger.log("warn", this.logPath, `[${this.id}]`, `\x1b[33m[warn]\x1b[0m`, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -93,6 +126,6 @@ export class Logger {
|
||||||
* @param args The data to print
|
* @param args The data to print
|
||||||
**/
|
**/
|
||||||
public debug(...args: any[]) {
|
public debug(...args: any[]) {
|
||||||
Logger.log("debug", `[${this.id}]`, `\x1b[32m[debug]\x1b[0m`, ...args);
|
Logger.log("debug", this.logPath, `[${this.id}]`, `\x1b[32m[debug]\x1b[0m`, ...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||||
import { parse, stringify } from "yaml";
|
import { parse, stringify } from "yaml";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file uses the synchronous functions from the fs
|
* This file uses the synchronous functions from the fs
|
||||||
|
@ -63,6 +62,7 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
|
||||||
return config as T;
|
return config as T;
|
||||||
} else {
|
} else {
|
||||||
// Write default config to disk and use that
|
// Write default config to disk and use that
|
||||||
|
//logger.warn(`Config file "${configPath}" not found, writing default config to disk`);
|
||||||
writeConfig(configPath, defaultConfig);
|
writeConfig(configPath, defaultConfig);
|
||||||
return defaultConfig as T;
|
return defaultConfig as T;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,9 @@ export function createUserID(ip: string) {
|
||||||
.update("::ffff:" + ip + env.SALT)
|
.update("::ffff:" + ip + env.SALT)
|
||||||
.digest("hex")
|
.digest("hex")
|
||||||
.substring(0, 24);
|
.substring(0, 24);
|
||||||
|
} else {
|
||||||
|
// Fallback if someone typed random garbage in the config
|
||||||
|
return createID();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import client, { Registry } from "prom-client";
|
||||||
|
import { Logger } from "./Logger";
|
||||||
|
|
||||||
|
const logger = new Logger("Metrics Server");
|
||||||
|
|
||||||
|
export function startMetricsServer() {
|
||||||
|
client.collectDefaultMetrics();
|
||||||
|
logger.info("Starting Prometheus metrics server...");
|
||||||
|
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: 9100,
|
||||||
|
async fetch(req) {
|
||||||
|
const res = new Response(await client.register.metrics());
|
||||||
|
res.headers.set("Content-Type", client.register.contentType);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
enableMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metrics = {
|
||||||
|
concurrentUsers: new client.Histogram({
|
||||||
|
name: "concurrent_users",
|
||||||
|
help: "Number of concurrent users",
|
||||||
|
}),
|
||||||
|
callbacks: [],
|
||||||
|
addCallback(callback: (...args: any[]) => void | Promise<void>) {
|
||||||
|
(this.callbacks as ((...args: any[]) => void)[]).push(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableMetrics() {
|
||||||
|
setInterval(() => {
|
||||||
|
(metrics.callbacks as ((...args: any[]) => void)[]).forEach(callback => callback());
|
||||||
|
}, 5000);
|
||||||
|
}
|
|
@ -3,23 +3,32 @@ import logger from "./logger";
|
||||||
import Command from "./Command";
|
import Command from "./Command";
|
||||||
import "./commands";
|
import "./commands";
|
||||||
|
|
||||||
export const rl = readline.createInterface({
|
export let rl: readline.Interface;
|
||||||
|
|
||||||
|
// Turned into a function so the import isn't in a weird spot
|
||||||
|
export function startReadline() {
|
||||||
|
rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout
|
output: process.stdout
|
||||||
});
|
});
|
||||||
|
|
||||||
rl.setPrompt("mpps> ");
|
rl.setPrompt("mpps> ");
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
|
|
||||||
rl.on("line", async line => {
|
rl.on("line", async line => {
|
||||||
const out = await Command.handleCommand(line);
|
const out = await Command.handleCommand(line);
|
||||||
logger.info(out);
|
logger.info(out);
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
});
|
});
|
||||||
|
|
||||||
rl.on("SIGINT", () => {
|
rl.on("SIGINT", () => {
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fucking cringe but it works
|
// Fucking cringe but it works
|
||||||
(globalThis as unknown as any).rl = rl;
|
(globalThis as unknown as any).rl = rl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopReadline() {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { loadConfig } from "./config";
|
||||||
|
|
||||||
|
export const config = loadConfig("config/util.yml", {
|
||||||
|
enableLogFiles: true
|
||||||
|
});
|
|
@ -11,65 +11,71 @@
|
||||||
* or, you know, maybe I could log their user agent
|
* or, you know, maybe I could log their user agent
|
||||||
* and IP address instead sometime in the future.
|
* and IP address instead sometime in the future.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Logger } from "../util/Logger";
|
||||||
|
|
||||||
|
const logger = new Logger("Socket Gateway");
|
||||||
|
|
||||||
export class Gateway {
|
export class Gateway {
|
||||||
// Whether we have correctly processed this socket's hi message
|
// Whether we have correctly processed this socket's hi message
|
||||||
public hasProcessedHi = false;
|
public hasProcessedHi = false; // implemented
|
||||||
|
|
||||||
// Whether they have sent the MIDI devices message
|
// Whether they have sent the MIDI devices message
|
||||||
public hasSentDevices = false;
|
public hasSentDevices = false; // implemented
|
||||||
|
|
||||||
// Whether they have sent a token
|
// Whether they have sent a token
|
||||||
public hasSentToken = false;
|
public hasSentToken = false; // implemented
|
||||||
|
|
||||||
// Whether their token is valid
|
// Whether their token is valid
|
||||||
public isTokenValid = false;
|
public isTokenValid = false; // implemented
|
||||||
|
|
||||||
// Their user agent, if sent
|
// Their user agent, if sent
|
||||||
public userAgent = "";
|
public userAgent = ""; // TODO
|
||||||
|
|
||||||
// Whether they have moved their cursor
|
// Whether they have moved their cursor
|
||||||
public hasCursorMoved = false;
|
public hasCursorMoved = false; // implemented
|
||||||
|
|
||||||
// Whether they sent a cursor message that contained numbers instead of stringified numbers
|
// Whether they sent a cursor message that contained numbers instead of stringified numbers
|
||||||
public isCursorNotString = false;
|
public isCursorNotString = false; // implemented
|
||||||
|
|
||||||
// The last time they sent a ping message
|
// The last time they sent a ping message
|
||||||
public lastPing = Date.now();
|
public lastPing = Date.now(); // implemented
|
||||||
|
|
||||||
// Whether they have joined any channel
|
// Whether they have joined any channel
|
||||||
public hasJoinedAnyChannel = false;
|
public hasJoinedAnyChannel = false; // implemented
|
||||||
|
|
||||||
// Whether they have joined the lobby
|
// Whether they have joined a lobby
|
||||||
public hasJoinedLobby = false;
|
public hasJoinedLobby = false; // implemented
|
||||||
|
|
||||||
// Whether they have made a regular non-websocket request to the HTTP server
|
// Whether they have made a regular non-websocket request to the HTTP server
|
||||||
// probably useful for checking if they are actually on the site
|
// probably useful for checking if they are actually on the site
|
||||||
// Maybe not useful if cloudflare is being used
|
// Maybe not useful if cloudflare is being used
|
||||||
// In that scenario, templating wouldn't work, either
|
// In that scenario, templating wouldn't work, either
|
||||||
public hasConnectedToHTTPServer = false;
|
public hasConnectedToHTTPServer = false; // implemented
|
||||||
|
|
||||||
// Various chat message flags
|
// Various chat message flags
|
||||||
public hasSentChatMessage = false;
|
public hasSentChatMessage = false; // implemented
|
||||||
public hasSentChatMessageWithCapitalLettersOnly = false;
|
public hasSentChatMessageWithCapitalLettersOnly = false; // implemented
|
||||||
public hasSentChatMessageWithInvisibleCharacters = false;
|
public hasSentChatMessageWithInvisibleCharacters = false; // implemented
|
||||||
public hasSentChatMessageWithEmoji = false;
|
public hasSentChatMessageWithEmoji = false; // implemented
|
||||||
|
|
||||||
// Whehter or not the user has played the piano in this session
|
// Whehter or not the user has played the piano in this session
|
||||||
public hasPlayedPianoBefore = false;
|
public hasPlayedPianoBefore = false; // implemented
|
||||||
|
|
||||||
// Whether the user has sent a channel list subscription request, a.k.a. opened the channel list
|
// Whether the user has sent a channel list subscription request, a.k.a. opened the channel list
|
||||||
public hasOpenedChannelList = false;
|
public hasOpenedChannelList = false; // implemented
|
||||||
|
|
||||||
// Whether the user has changed their name/color this session (not just changed from default)
|
// Whether the user has changed their name/color this session (not just changed from default)
|
||||||
public hasChangedName = false;
|
public hasChangedName = false; // implemented
|
||||||
public hasChangedColor = false;
|
public hasChangedColor = false; // implemented
|
||||||
|
|
||||||
// Whether the user has sent
|
|
||||||
public hasSentCustomNoteData = false;
|
|
||||||
|
|
||||||
// Whether they sent an admin message that was invalid (wrong password, etc)
|
// Whether they sent an admin message that was invalid (wrong password, etc)
|
||||||
public hasSentInvalidAdminMessage = false;
|
public hasSentInvalidAdminMessage = false; // implemented
|
||||||
|
|
||||||
// Whether or not they have passed the b message
|
// Whether or not they have passed the b message
|
||||||
public hasCompletedBrowserChallenge = false;
|
public hasCompletedBrowserChallenge = false; // implemented
|
||||||
|
|
||||||
|
public dump() {
|
||||||
|
return JSON.stringify(this, undefined, 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,7 @@ export class Socket extends EventEmitter {
|
||||||
// Basic function
|
// Basic function
|
||||||
this.sendArray([{
|
this.sendArray([{
|
||||||
m: "b",
|
m: "b",
|
||||||
code: `~return true;`
|
code: `~return btoa(JSON.stringify([true, navigator.userAgent]));`
|
||||||
}]);
|
}]);
|
||||||
} else if (config.browserChallenge == "obf") {
|
} else if (config.browserChallenge == "obf") {
|
||||||
// Obfuscated challenge building
|
// Obfuscated challenge building
|
||||||
|
@ -183,7 +183,7 @@ export class Socket extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move this participant to a channel
|
* Move this socket to a channel
|
||||||
* @param _id Target channel ID
|
* @param _id Target channel ID
|
||||||
* @param set Channel settings, if the channel is instantiated
|
* @param set Channel settings, if the channel is instantiated
|
||||||
* @param force Whether to make this socket join regardless of channel properties
|
* @param force Whether to make this socket join regardless of channel properties
|
||||||
|
@ -239,6 +239,17 @@ export class Socket extends EventEmitter {
|
||||||
// Make them join the new channel
|
// Make them join the new channel
|
||||||
channel.join(this, force);
|
channel.join(this, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gateway stuff
|
||||||
|
this.gateway.hasJoinedAnyChannel = true;
|
||||||
|
|
||||||
|
const ch = this.getCurrentChannel();
|
||||||
|
|
||||||
|
if (ch) {
|
||||||
|
if (ch.isLobby()) {
|
||||||
|
this.gateway.hasJoinedLobby = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public admin = new EventEmitter();
|
public admin = new EventEmitter();
|
||||||
|
|
|
@ -16,6 +16,8 @@ export const plus_ls: ServerEventListener<"+ls"> = {
|
||||||
if (!socket.rateLimits.normal["+ls"].attempt()) return;
|
if (!socket.rateLimits.normal["+ls"].attempt()) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
socket.gateway.hasOpenedChannelList = true;
|
||||||
|
|
||||||
socket.subscribeToChannelList();
|
socket.subscribeToChannelList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,28 @@
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
import { Socket } from "../../../Socket";
|
||||||
|
import { ServerEventListener, ServerEvents } from "../../../../util/types";
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/64509631/is-there-a-regex-to-match-all-unicode-emojis
|
||||||
|
const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g;
|
||||||
|
|
||||||
|
function populateSocketChatGatewayFlags(msg: ServerEvents["a"], socket: Socket) {
|
||||||
|
socket.gateway.hasSentChatMessage = true;
|
||||||
|
|
||||||
|
if (msg.message.toUpperCase() == msg.message) {
|
||||||
|
socket.gateway.hasSentChatMessageWithCapitalLettersOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.message.includes("\u034f") || msg.message.includes("\u200b")) {
|
||||||
|
socket.gateway.hasSentChatMessageWithInvisibleCharacters = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.message.match(/[^\x00-\x7f]/gm)) {
|
||||||
|
socket.gateway.hasSentChatMessageWithInvisibleCharacters = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.message.match(emojiRegex)) {
|
||||||
|
socket.gateway.hasSentChatMessageWithEmoji = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const a: ServerEventListener<"a"> = {
|
export const a: ServerEventListener<"a"> = {
|
||||||
id: "a",
|
id: "a",
|
||||||
|
@ -7,9 +31,14 @@ export const a: ServerEventListener<"a"> = {
|
||||||
const flags = socket.getUserFlags();
|
const flags = socket.getUserFlags();
|
||||||
if (!flags) return;
|
if (!flags) return;
|
||||||
|
|
||||||
|
if (typeof msg.message !== "string") return;
|
||||||
|
|
||||||
// Why did I write this statement so weird
|
// Why did I write this statement so weird
|
||||||
if (!flags["no chat rate limit"] || flags["no chat rate limit"] == 0)
|
if (!flags["no chat rate limit"] || flags["no chat rate limit"] == 0)
|
||||||
if (!socket.rateLimits?.normal.a.attempt()) return;
|
if (!socket.rateLimits?.normal.a.attempt()) return;
|
||||||
|
|
||||||
|
populateSocketChatGatewayFlags(msg, socket);
|
||||||
|
|
||||||
const ch = socket.getCurrentChannel();
|
const ch = socket.getCurrentChannel();
|
||||||
if (!ch) return;
|
if (!ch) return;
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,15 @@ export const admin_message: ServerEventListener<"admin message"> = {
|
||||||
if (socket.rateLimits)
|
if (socket.rateLimits)
|
||||||
if (!socket.rateLimits.normal["admin message"].attempt()) return;
|
if (!socket.rateLimits.normal["admin message"].attempt()) return;
|
||||||
|
|
||||||
if (typeof msg.password !== "string") return;
|
if (typeof msg.password !== "string") {
|
||||||
if (msg.password !== env.ADMIN_PASS) return;
|
socket.gateway.hasSentInvalidAdminMessage = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.password !== env.ADMIN_PASS) {
|
||||||
|
socket.gateway.hasSentInvalidAdminMessage = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Probably shouldn't be using password auth in 2024
|
// Probably shouldn't be using password auth in 2024
|
||||||
// Maybe I'll setup a dashboard instead some day
|
// Maybe I'll setup a dashboard instead some day
|
||||||
|
|
|
@ -17,10 +17,20 @@ export const hi: ServerEventListener<"hi"> = {
|
||||||
|
|
||||||
// Browser challenge
|
// Browser challenge
|
||||||
if (config.browserChallenge == "basic") {
|
if (config.browserChallenge == "basic") {
|
||||||
if (typeof msg.code !== "boolean") return;
|
try {
|
||||||
|
if (typeof msg.code !== "string") return;
|
||||||
|
const code = atob(msg.code);
|
||||||
|
const arr = JSON.parse(code);
|
||||||
|
|
||||||
if (msg.code === true) {
|
if (arr[0] === true) {
|
||||||
socket.gateway.hasCompletedBrowserChallenge = true;
|
socket.gateway.hasCompletedBrowserChallenge = true;
|
||||||
|
|
||||||
|
if (typeof arr[1] === "string") {
|
||||||
|
socket.gateway.userAgent = arr[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn("Unable to parse basic browser challenge code:", err);
|
||||||
}
|
}
|
||||||
} else if (config.browserChallenge == "obf") {
|
} else if (config.browserChallenge == "obf") {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -34,11 +44,14 @@ export const hi: ServerEventListener<"hi"> = {
|
||||||
|
|
||||||
if (config.tokenAuth !== "none") {
|
if (config.tokenAuth !== "none") {
|
||||||
if (typeof msg.token !== "string") {
|
if (typeof msg.token !== "string") {
|
||||||
|
socket.gateway.hasSentToken = true;
|
||||||
|
|
||||||
// Get a saved token
|
// Get a saved token
|
||||||
token = await getToken(socket.getUserID());
|
token = await getToken(socket.getUserID());
|
||||||
if (typeof token !== "string") {
|
if (typeof token !== "string") {
|
||||||
// Generate a new one
|
// Generate a new one
|
||||||
token = await createToken(socket.getUserID(), socket.gateway);
|
token = await createToken(socket.getUserID(), socket.gateway);
|
||||||
|
socket.gateway.isTokenValid = true;
|
||||||
|
|
||||||
if (typeof token !== "string") {
|
if (typeof token !== "string") {
|
||||||
logger.warn(`Unable to generate token for user ${socket.getUserID()}`);
|
logger.warn(`Unable to generate token for user ${socket.getUserID()}`);
|
||||||
|
@ -54,6 +67,7 @@ export const hi: ServerEventListener<"hi"> = {
|
||||||
//return;
|
//return;
|
||||||
} else {
|
} else {
|
||||||
token = msg.token;
|
token = msg.token;
|
||||||
|
socket.gateway.isTokenValid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,21 @@ export const m: ServerEventListener<"m"> = {
|
||||||
let x = msg.x;
|
let x = msg.x;
|
||||||
let y = msg.y;
|
let y = msg.y;
|
||||||
|
|
||||||
// Make it numbers
|
// Parse cursor position if it's strings
|
||||||
if (typeof msg.x == "string") x = parseFloat(msg.x);
|
if (typeof msg.x == "string") {
|
||||||
if (typeof msg.y == "string") y = parseFloat(msg.y);
|
x = parseFloat(msg.x);
|
||||||
|
} else {
|
||||||
|
socket.gateway.isCursorNotString = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Move the laggy piece of shit
|
if (typeof msg.y == "string") {
|
||||||
|
y = parseFloat(msg.y);
|
||||||
|
} else {
|
||||||
|
socket.gateway.isCursorNotString = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relocate the laggy microscopic speck
|
||||||
socket.setCursorPos(x, y);
|
socket.setCursorPos(x, y);
|
||||||
|
socket.gateway.hasCursorMoved = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const n: ServerEventListener<"n"> = {
|
||||||
if (!Array.isArray(msg.n)) return;
|
if (!Array.isArray(msg.n)) return;
|
||||||
if (typeof msg.t !== "number") return;
|
if (typeof msg.t !== "number") return;
|
||||||
|
|
||||||
|
socket.gateway.hasPlayedPianoBefore = true;
|
||||||
|
|
||||||
// This should've been here months ago
|
// This should've been here months ago
|
||||||
const channel = socket.getCurrentChannel();
|
const channel = socket.getCurrentChannel();
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
|
|
|
@ -12,6 +12,8 @@ export const t: ServerEventListener<"t"> = {
|
||||||
if (typeof msg.e !== "number") return;
|
if (typeof msg.e !== "number") return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
socket.gateway.lastPing = Date.now();
|
||||||
|
|
||||||
// Pong
|
// Pong
|
||||||
socket.sendArray([
|
socket.sendArray([
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
import { ServerEventListener } from "../../../../util/types";
|
||||||
|
import { config } from "../../../usersConfig";
|
||||||
|
|
||||||
export const userset: ServerEventListener<"userset"> = {
|
export const userset: ServerEventListener<"userset"> = {
|
||||||
id: "userset",
|
id: "userset",
|
||||||
callback: async (msg, socket) => {
|
callback: async (msg, socket) => {
|
||||||
// Change username/color
|
// Change username/color
|
||||||
if (!socket.rateLimits?.chains.userset.attempt()) return;
|
if (!socket.rateLimits?.chains.userset.attempt()) return;
|
||||||
// You can disable color in the config because
|
if (typeof msg.set.name !== "string" && typeof msg.set.color !== "string") return;
|
||||||
// Brandon's/jacored's server doesn't allow color changes,
|
|
||||||
// and that's the OG server, but folks over at MPP.net
|
if (typeof msg.set.name == "string") {
|
||||||
// said otherwise because they're dumb roleplayers
|
socket.gateway.hasChangedName = true;
|
||||||
// or something and don't understand the unique value
|
}
|
||||||
// of the fishing bot and how it allows you to change colors
|
|
||||||
// without much control, giving it the feeling of value...
|
if (typeof msg.set.color == "string" && config.enableColorChanging) {
|
||||||
// Kinda reminds me of crypto.
|
socket.gateway.hasChangedColor = true;
|
||||||
// Also, Brandon's server had color changing on before.
|
}
|
||||||
if (!msg.set.name && !msg.set.color) return;
|
|
||||||
socket.userset(msg.set.name, msg.set.color);
|
socket.userset(msg.set.name, msg.set.color);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,9 +7,14 @@ import { Socket, socketsBySocketID } from "./Socket";
|
||||||
import env from "../util/env";
|
import env from "../util/env";
|
||||||
import { getMOTD } from "../util/motd";
|
import { getMOTD } from "../util/motd";
|
||||||
import nunjucks from "nunjucks";
|
import nunjucks from "nunjucks";
|
||||||
|
import { metrics } from "../util/metrics";
|
||||||
|
|
||||||
const logger = new Logger("WebSocket Server");
|
const logger = new Logger("WebSocket Server");
|
||||||
|
|
||||||
|
// ip -> timestamp
|
||||||
|
// for checking if they visited the site and are also connected to the websocket
|
||||||
|
const httpIpCache = new Map<string, number>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a rendered version of the index file
|
* Get a rendered version of the index file
|
||||||
* @returns Response with html in it
|
* @returns Response with html in it
|
||||||
|
@ -39,17 +44,15 @@ export const app = Bun.serve<{ ip: string }>({
|
||||||
fetch: (req, server) => {
|
fetch: (req, server) => {
|
||||||
const reqip = server.requestIP(req);
|
const reqip = server.requestIP(req);
|
||||||
if (!reqip) return;
|
if (!reqip) return;
|
||||||
|
|
||||||
const ip = req.headers.get("x-forwarded-for") || reqip.address;
|
const ip = req.headers.get("x-forwarded-for") || reqip.address;
|
||||||
|
|
||||||
if (
|
// Upgrade websocket connections
|
||||||
server.upgrade(req, {
|
if (server.upgrade(req, { data: { ip } })) {
|
||||||
data: {
|
|
||||||
ip
|
|
||||||
}
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
httpIpCache.set(ip, Date.now());
|
||||||
const url = new URL(req.url).pathname;
|
const url = new URL(req.url).pathname;
|
||||||
|
|
||||||
// lol
|
// lol
|
||||||
|
@ -67,37 +70,55 @@ export const app = Bun.serve<{ ip: string }>({
|
||||||
|
|
||||||
// Time for unreadable blocks of confusion
|
// Time for unreadable blocks of confusion
|
||||||
try {
|
try {
|
||||||
|
// Is it a file?
|
||||||
if (fs.lstatSync(file).isFile()) {
|
if (fs.lstatSync(file).isFile()) {
|
||||||
|
// Read the file
|
||||||
const data = Bun.file(file);
|
const data = Bun.file(file);
|
||||||
|
|
||||||
|
// Return the file
|
||||||
if (data) {
|
if (data) {
|
||||||
return new Response(data);
|
return new Response(data);
|
||||||
} else {
|
} else {
|
||||||
return getIndex();
|
return getIndex();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Return the index file, since it's a channel name or something
|
||||||
return getIndex();
|
return getIndex();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Return the index file as a coverup of our extreme failure
|
||||||
return getIndex();
|
return getIndex();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
websocket: {
|
websocket: {
|
||||||
open: ws => {
|
open: ws => {
|
||||||
// We got one!
|
// swimming in the pool
|
||||||
const socket = new Socket(ws, createSocketID());
|
const socket = new Socket(ws, createSocketID());
|
||||||
|
|
||||||
// Reel 'em in...
|
|
||||||
(ws as unknown as any).socket = socket;
|
(ws as unknown as any).socket = socket;
|
||||||
// logger.debug("Connection at " + socket.getIP());
|
// logger.debug("Connection at " + socket.getIP());
|
||||||
|
|
||||||
// Let's put it in the dinner bucket.
|
|
||||||
if (socket.socketID == undefined) {
|
if (socket.socketID == undefined) {
|
||||||
socket.socketID = createSocketID();
|
socket.socketID = createSocketID();
|
||||||
}
|
}
|
||||||
|
|
||||||
socketsBySocketID.set(socket.socketID, socket);
|
socketsBySocketID.set(socket.socketID, socket);
|
||||||
|
|
||||||
|
const ip = socket.getIP();
|
||||||
|
|
||||||
|
if (httpIpCache.has(ip)) {
|
||||||
|
const date = httpIpCache.get(ip);
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
if (Date.now() - date < 1000 * 60) {
|
||||||
|
// They got the page and we were connected in under a minute
|
||||||
|
socket.gateway.hasConnectedToHTTPServer = true;
|
||||||
|
} else {
|
||||||
|
// They got the page and a long time has passed
|
||||||
|
httpIpCache.delete(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
message: (ws, message) => {
|
message: (ws, message) => {
|
||||||
|
|
Loading…
Reference in New Issue