Newer
Older
import http from "node:http";
import {
ClientConfig,
ClientToServerEvents,
Pixel,
ServerToClientEvents,
} from "@sc07-canvas/lib/src/net";
import { Server, Socket as RawSocket } from "socket.io";
import { session } from "./Express";
import Canvas from "./Canvas";
import { PalleteColor } from "@prisma/client";
import { prisma } from "./prisma";
import { Logger } from "./Logger";
import { Redis } from "./redis";
import { User } from "../models/User";
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* get socket.io server config, generated from environment vars
*/
const getSocketConfig = () => {
// origins that should be permitted
// origins need to be specifically defined if we want to allow CORS credential usage (cookies)
const origins: string[] = [];
if (process.env.CLIENT_ORIGIN) {
origins.push(process.env.CLIENT_ORIGIN);
}
if (origins.length === 0) {
return undefined;
}
return {
cors: {
origin: origins,
credentials: true,
},
};
};
// this is terrible, another way to get the client config needs to be found
let PALLETE: PalleteColor[] = [];
const PIXEL_TIMEOUT_MS = 1000;
prisma.palleteColor
.findMany()
.then((palleteColors) => {
PALLETE = palleteColors;
Logger.info(`Loaded ${palleteColors.length} pallete colors`);
})
.catch((e) => {
Logger.error("Failed to get pallete colors", e);
});
const getClientConfig = (): ClientConfig => {
return {
pallete: {
colors: PALLETE,
pixel_cooldown: PIXEL_TIMEOUT_MS,
},
canvas: Canvas.getCanvasConfig(),
};
};
type Socket = RawSocket<ClientToServerEvents, ServerToClientEvents>;
export class SocketServer {
io: Server<ClientToServerEvents, ServerToClientEvents>;
constructor(server: http.Server) {
this.io = new Server(server, getSocketConfig());
this.io.engine.use(session);
this.io.on("connection", this.handleConnection.bind(this));
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// pixel stacking
// - needs to be exponential (takes longer to aquire more pixels stacked)
// - convert to config options instead of hard-coded
setInterval(async () => {
Logger.debug("Running pixel stacking...");
const redis = await Redis.getClient();
const sockets = await this.io.local.fetchSockets();
for (const socket of sockets) {
const sub = await redis.get(Redis.key("socketToSub", socket.id));
if (!sub) {
Logger.warn(`Socket ${socket.id} has no user`);
continue;
}
const user = await User.fromSub(sub);
if (!user) {
Logger.warn(
`Socket ${socket.id}'s user (${sub}) does not exist in the database`
);
continue;
}
// time in seconds since last pixel placement
// TODO: this causes a mismatch between placement times
// - going from 0 stack to 6 stack has a steady increase between each
// - going from 3 stack to 6 stack takes longer
const timeSinceLastPlace =
(Date.now() - user.lastPixelTime.getTime()) / 1000;
const cooldown = CanvasLib.getPixelCooldown(
user.pixelStack + 1,
getClientConfig()
);
// this impl has the side affect of giving previously offline users all the stack upon reconnecting
if (
timeSinceLastPlace >= cooldown &&
user.pixelStack < getClientConfig().canvas.pixel.maxStack
) {
await user.modifyStack(1);
Logger.debug(sub + " has gained another pixel in their stack");
}
}
}, 1000);
async handleConnection(socket: Socket) {
const user =
socket.request.session.user &&
(await User.fromAuthSession(socket.request.session.user));
Logger.debug(
`Socket ${socket.id} connection ` + (user ? "@" + user.sub : "No Auth")
);
user?.sockets.add(socket);
Logger.debug("handleConnection " + user?.sockets.size);
Redis.getClient().then((redis) => {
if (user) redis.set(Redis.key("socketToSub", socket.id), user.sub);
});
if (socket.request.session.user) {
// inform the client of their session if it exists
socket.emit("user", socket.request.session.user);
}
if (user) {
socket.emit("availablePixels", user.pixelStack);
socket.emit("pixelLastPlaced", user.lastPixelTime.getTime());
}
socket.emit("config", getClientConfig());
Canvas.getPixelsArray().then((pixels) => {
socket.emit("canvas", pixels);
});
socket.on("disconnect", () => {
Logger.debug(`Socket ${socket.id} disconnected`);
user?.sockets.delete(socket);
Redis.getClient().then((redis) => {
if (user) redis.del(Redis.key("socketToSub", socket.id));
});
});
socket.on("place", async (pixel, ack) => {
if (!user) {
ack({ success: false, error: "no_user" });
return;
}
if (
pixel.x < 0 ||
pixel.y < 0 ||
pixel.x >= getClientConfig().canvas.size[0] ||
pixel.y >= getClientConfig().canvas.size[1]
) {
ack({ success: false, error: "invalid_pixel" });
return;
// force a user data update
await user.update(true);
if (user.pixelStack < 1) {
ack({ success: false, error: "pixel_cooldown" });
return;
}
await user.modifyStack(-1);
const paletteColor = await prisma.palleteColor.findFirst({
where: {
id: pixel.color,
},
});
await Canvas.setPixel(user, pixel.x, pixel.y, paletteColor.hex);
const newPixel: Pixel = {
x: pixel.x,
y: pixel.y,
color: pixel.color,
};
ack({
success: true,
data: newPixel,
});
socket.broadcast.emit("pixel", newPixel);
});
}
/**
* Master Shard (need better name)
* This shard should be in charge of all user management, allowing for syncronized events
*
* Events:
* - online people announcement
*
* this does work with multiple socket.io instances, so this needs to only be executed by one shard
*/
// possible issue: this includes every connected socket, not user count