Loading packages/client/src/lib/canvas.ts +3 −0 Original line number Diff line number Diff line Loading @@ -403,6 +403,9 @@ export class Canvas extends EventEmitter<CanvasEvents> { case "no_user": toast.error("You are not logged in."); break; case "pixel_already_pending": toast.error("You are already placing a pixel"); break; case "palette_color_invalid": toast.error("This isn't a color that you can use...?"); break; Loading packages/lib/src/canvas.ts +6 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,12 @@ export const CanvasLib = new (class { // oh god last minute change to match activity cooldown // 100 = user count // band aid over negative nums if (pixelNumber < 1) { pixelNumber = 1 } return (2.5 * Math.sqrt(100 + 11.96) + 6.5) * 1 * pixelNumber; } })(); packages/lib/src/net.ts +1 −0 Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ export interface ClientToServerEvents { | "palette_color_invalid" | "you_already_placed_that" | "banned" | "pixel_already_pending" > ) => void ) => void; Loading packages/server/src/lib/SocketServer.ts +43 −0 Original line number Diff line number Diff line Loading @@ -85,6 +85,13 @@ type Socket = RawSocket<ClientToServerEvents, ServerToClientEvents>; export class SocketServer { static instance: SocketServer; io: Server<ClientToServerEvents, ServerToClientEvents>; /** * Prevent users from time attacking pixel placements to place more pixels than stacked * * @key user sub (grant@grants.cafe) * @value timestamp */ userPlaceLock = new Map<string, number>(); constructor(server: http.Server) { SocketServer.instance = this; Loading @@ -96,6 +103,29 @@ export class SocketServer { this.io.engine.use(session); this.io.on("connection", this.handleConnection.bind(this)); // clear pixel locks if they have existed for more than a minute setInterval(() => { const oneMinuteAgo = new Date(); oneMinuteAgo.setMinutes(oneMinuteAgo.getMinutes() - 1); const expired = [...this.userPlaceLock.entries()].filter( ([user, time]) => time < oneMinuteAgo.getTime() ); if (expired.length > 0) { Logger.warn( "A pixel lock has existed for too long for " + expired.length + " users : " + expired.map((a) => a[0]).join(",") ); } for (const expire of expired) { this.userPlaceLock.delete(expire[0]); } }, 1000 * 30); // pixel stacking // - needs to be exponential (takes longer to aquire more pixels stacked) // - convert to config options instead of hard-coded Loading Loading @@ -254,19 +284,29 @@ export class SocketServer { // force a user data update await user.update(true); if (this.userPlaceLock.has(user.sub)) { ack({ success: false, error: "pixel_already_pending" }); return; } this.userPlaceLock.set(user.sub, Date.now()); if (bypassCooldown && !user.isModerator) { // only moderators can do this ack({ success: false, error: "invalid_pixel" }); this.userPlaceLock.delete(user.sub); return; } if (!bypassCooldown && user.pixelStack < 1) { ack({ success: false, error: "pixel_cooldown" }); this.userPlaceLock.delete(user.sub); return; } if ((user.getBan()?.expires || 0) > new Date()) { ack({ success: false, error: "banned" }); this.userPlaceLock.delete(user.sub); return; } Loading @@ -280,6 +320,7 @@ export class SocketServer { success: false, error: "palette_color_invalid", }); this.userPlaceLock.delete(user.sub); return; } Loading @@ -291,6 +332,7 @@ export class SocketServer { pixelAtTheSameLocation.color === paletteColor.hex ) { ack({ success: false, error: "you_already_placed_that" }); this.userPlaceLock.delete(user.sub); return; } Loading Loading @@ -319,6 +361,7 @@ export class SocketServer { data: newPixel, }); socket.broadcast.emit("pixel", newPixel); this.userPlaceLock.delete(user.sub); }); socket.on("undo", async (ack) => { Loading Loading
packages/client/src/lib/canvas.ts +3 −0 Original line number Diff line number Diff line Loading @@ -403,6 +403,9 @@ export class Canvas extends EventEmitter<CanvasEvents> { case "no_user": toast.error("You are not logged in."); break; case "pixel_already_pending": toast.error("You are already placing a pixel"); break; case "palette_color_invalid": toast.error("This isn't a color that you can use...?"); break; Loading
packages/lib/src/canvas.ts +6 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,12 @@ export const CanvasLib = new (class { // oh god last minute change to match activity cooldown // 100 = user count // band aid over negative nums if (pixelNumber < 1) { pixelNumber = 1 } return (2.5 * Math.sqrt(100 + 11.96) + 6.5) * 1 * pixelNumber; } })();
packages/lib/src/net.ts +1 −0 Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ export interface ClientToServerEvents { | "palette_color_invalid" | "you_already_placed_that" | "banned" | "pixel_already_pending" > ) => void ) => void; Loading
packages/server/src/lib/SocketServer.ts +43 −0 Original line number Diff line number Diff line Loading @@ -85,6 +85,13 @@ type Socket = RawSocket<ClientToServerEvents, ServerToClientEvents>; export class SocketServer { static instance: SocketServer; io: Server<ClientToServerEvents, ServerToClientEvents>; /** * Prevent users from time attacking pixel placements to place more pixels than stacked * * @key user sub (grant@grants.cafe) * @value timestamp */ userPlaceLock = new Map<string, number>(); constructor(server: http.Server) { SocketServer.instance = this; Loading @@ -96,6 +103,29 @@ export class SocketServer { this.io.engine.use(session); this.io.on("connection", this.handleConnection.bind(this)); // clear pixel locks if they have existed for more than a minute setInterval(() => { const oneMinuteAgo = new Date(); oneMinuteAgo.setMinutes(oneMinuteAgo.getMinutes() - 1); const expired = [...this.userPlaceLock.entries()].filter( ([user, time]) => time < oneMinuteAgo.getTime() ); if (expired.length > 0) { Logger.warn( "A pixel lock has existed for too long for " + expired.length + " users : " + expired.map((a) => a[0]).join(",") ); } for (const expire of expired) { this.userPlaceLock.delete(expire[0]); } }, 1000 * 30); // pixel stacking // - needs to be exponential (takes longer to aquire more pixels stacked) // - convert to config options instead of hard-coded Loading Loading @@ -254,19 +284,29 @@ export class SocketServer { // force a user data update await user.update(true); if (this.userPlaceLock.has(user.sub)) { ack({ success: false, error: "pixel_already_pending" }); return; } this.userPlaceLock.set(user.sub, Date.now()); if (bypassCooldown && !user.isModerator) { // only moderators can do this ack({ success: false, error: "invalid_pixel" }); this.userPlaceLock.delete(user.sub); return; } if (!bypassCooldown && user.pixelStack < 1) { ack({ success: false, error: "pixel_cooldown" }); this.userPlaceLock.delete(user.sub); return; } if ((user.getBan()?.expires || 0) > new Date()) { ack({ success: false, error: "banned" }); this.userPlaceLock.delete(user.sub); return; } Loading @@ -280,6 +320,7 @@ export class SocketServer { success: false, error: "palette_color_invalid", }); this.userPlaceLock.delete(user.sub); return; } Loading @@ -291,6 +332,7 @@ export class SocketServer { pixelAtTheSameLocation.color === paletteColor.hex ) { ack({ success: false, error: "you_already_placed_that" }); this.userPlaceLock.delete(user.sub); return; } Loading Loading @@ -319,6 +361,7 @@ export class SocketServer { data: newPixel, }); socket.broadcast.emit("pixel", newPixel); this.userPlaceLock.delete(user.sub); }); socket.on("undo", async (ack) => { Loading