reorganize server & fix various linting errors
This commit is contained in:
parent
a4b3adaace
commit
c3b8467b8f
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"eslint.workingDirectories": [
|
||||||
|
"packages/server",
|
||||||
|
"packages/client",
|
||||||
|
"packages/admin",
|
||||||
|
"packages/lib"
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
||||||
|
dist
|
|
@ -22,6 +22,7 @@
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@nextui-org/react": "^2.2.9",
|
"@nextui-org/react": "^2.2.9",
|
||||||
"@sc07-canvas/lib": "^1.0.0",
|
"@sc07-canvas/lib": "^1.0.0",
|
||||||
|
"@typescript-eslint/parser": "^7.1.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"framer-motion": "^11.0.5",
|
"framer-motion": "^11.0.5",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Socket, io } from "socket.io-client";
|
import { Socket, io } from "socket.io-client";
|
||||||
|
import EventEmitter from "eventemitter3";
|
||||||
import {
|
import {
|
||||||
|
AuthSession,
|
||||||
ClientConfig,
|
ClientConfig,
|
||||||
ClientToServerEvents,
|
ClientToServerEvents,
|
||||||
ServerToClientEvents,
|
ServerToClientEvents,
|
||||||
} from "../types";
|
} from "@sc07-canvas/lib/src/net";
|
||||||
import EventEmitter from "eventemitter3";
|
|
||||||
import { AuthSession } from "@sc07-canvas/lib/src/net";
|
|
||||||
|
|
||||||
export interface INetworkEvents {
|
export interface INetworkEvents {
|
||||||
user: (user: AuthSession) => void;
|
user: (user: AuthSession) => void;
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
import { AuthSession, PacketAck } from "@sc07-canvas/lib/src/net";
|
|
||||||
|
|
||||||
// socket.io
|
|
||||||
|
|
||||||
export interface ServerToClientEvents {
|
|
||||||
canvas: (pixels: string[]) => void;
|
|
||||||
user: (user: AuthSession) => void;
|
|
||||||
config: (config: ClientConfig) => void;
|
|
||||||
pixel: (pixel: Pixel) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientToServerEvents {
|
|
||||||
place: (pixel: Pixel, ack: (_: PacketAck<Pixel>) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// app context
|
|
||||||
|
|
||||||
export interface IAppContext {
|
|
||||||
config: ClientConfig;
|
|
||||||
user?: AuthSession;
|
|
||||||
canvasPosition?: ICanvasPosition;
|
|
||||||
setCanvasPosition: (v: ICanvasPosition) => void;
|
|
||||||
cursorPosition?: IPosition;
|
|
||||||
setCursorPosition: (v?: IPosition) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPalleteContext {
|
|
||||||
color?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICanvasPosition {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
zoom: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPosition {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// other
|
|
||||||
|
|
||||||
export type Pixel = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
color: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PalleteColor = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
hex: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CanvasConfig = {
|
|
||||||
size: [number, number];
|
|
||||||
zoom: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ClientConfig = {
|
|
||||||
pallete: {
|
|
||||||
colors: PalleteColor[];
|
|
||||||
pixel_cooldown: number;
|
|
||||||
};
|
|
||||||
canvas: CanvasConfig;
|
|
||||||
};
|
|
|
@ -1,42 +1,71 @@
|
||||||
export type CPixelPacket = ClientPacket & {
|
// socket.io
|
||||||
type: "place";
|
|
||||||
|
export interface ServerToClientEvents {
|
||||||
|
canvas: (pixels: string[]) => void;
|
||||||
|
user: (user: AuthSession) => void;
|
||||||
|
config: (config: ClientConfig) => void;
|
||||||
|
pixel: (pixel: Pixel) => void;
|
||||||
|
online: (count: { count: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientToServerEvents {
|
||||||
|
place: (pixel: Pixel, ack: (_: PacketAck<Pixel>) => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app context
|
||||||
|
|
||||||
|
export interface IAppContext {
|
||||||
|
config: ClientConfig;
|
||||||
|
user?: AuthSession;
|
||||||
|
canvasPosition?: ICanvasPosition;
|
||||||
|
setCanvasPosition: (v: ICanvasPosition) => void;
|
||||||
|
cursorPosition?: IPosition;
|
||||||
|
setCursorPosition: (v?: IPosition) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPalleteContext {
|
||||||
|
color?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICanvasPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
zoom: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// other
|
||||||
|
|
||||||
|
export type Pixel = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
color: number;
|
color: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SCanvasPacket = ServerPacket & {
|
export type PalleteColor = {
|
||||||
type: "canvas";
|
id: number;
|
||||||
pixels: string[];
|
name: string;
|
||||||
|
hex: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SPixelPacket = ServerPacket & {
|
export type CanvasConfig = {
|
||||||
type: "pixel";
|
size: [number, number];
|
||||||
x: number;
|
zoom: number;
|
||||||
y: number;
|
|
||||||
color: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SUserPacket = ServerPacket & {
|
export type ClientConfig = {
|
||||||
type: "user";
|
pallete: {
|
||||||
user: AuthSession;
|
colors: PalleteColor[];
|
||||||
|
pixel_cooldown: number;
|
||||||
|
};
|
||||||
|
canvas: CanvasConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Packet = {
|
export type PacketAck<T> =
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// server -> client
|
|
||||||
export type ServerPacket = Packet & {
|
|
||||||
_direction: "server->client";
|
|
||||||
};
|
|
||||||
|
|
||||||
// client -> server
|
|
||||||
export type ClientPacket = Packet & {
|
|
||||||
_direction: "client->server";
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PacketAck<T = ServerPacket> =
|
|
||||||
| {
|
| {
|
||||||
success: true;
|
success: true;
|
||||||
data: T;
|
data: T;
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"es2021": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"files": [".eslintrc.{js,cjs}"],
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "script"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"rules": {
|
||||||
|
"no-console": "error",
|
||||||
|
"@typescript-eslint/no-namespace": "off"
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,8 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "DOTENV_CONFIG_PATH=.env.local nodemon -r dotenv/config src/index.ts"
|
"dev": "DOTENV_CONFIG_PATH=.env.local nodemon -r dotenv/config src/index.ts",
|
||||||
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -12,7 +13,10 @@
|
||||||
"@tsconfig/recommended": "^1.0.2",
|
"@tsconfig/recommended": "^1.0.2",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/express-session": "^1.17.7",
|
"@types/express-session": "^1.17.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||||
|
"@typescript-eslint/parser": "^7.1.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"prettier": "^3.0.1",
|
"prettier": "^3.0.1",
|
||||||
"prisma": "^5.3.1",
|
"prisma": "^5.3.1",
|
||||||
|
@ -27,6 +31,7 @@
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"redis": "^4.6.12",
|
"redis": "^4.6.12",
|
||||||
"socket.io": "^4.7.2"
|
"socket.io": "^4.7.2",
|
||||||
|
"winston": "^3.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,10 @@ const AUTH_ENDPOINT = "https://auth.fediverse.events";
|
||||||
const AUTH_CLIENT = "canvas";
|
const AUTH_CLIENT = "canvas";
|
||||||
const AUTH_SECRET = "secret";
|
const AUTH_SECRET = "secret";
|
||||||
|
|
||||||
|
app.get("/me", (req, res) => {
|
||||||
|
res.json(req.session);
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/login", (req, res) => {
|
app.get("/login", (req, res) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set("service", "canvas");
|
params.set("service", "canvas");
|
||||||
|
|
|
@ -1,192 +1,42 @@
|
||||||
import express from "express";
|
|
||||||
import expressSession from "express-session";
|
|
||||||
import http from "node:http";
|
|
||||||
import { Server } from "socket.io";
|
|
||||||
import {
|
|
||||||
AuthSession,
|
|
||||||
CPixelPacket,
|
|
||||||
PacketAck,
|
|
||||||
SCanvasPacket,
|
|
||||||
SPixelPacket,
|
|
||||||
SUserPacket,
|
|
||||||
} from "@sc07-canvas/lib/src/net";
|
|
||||||
import APIRoutes from "./api";
|
|
||||||
|
|
||||||
// load declare module
|
// load declare module
|
||||||
import "./types";
|
import "./types";
|
||||||
import { prisma } from "./lib/prisma";
|
import { Redis } from "./lib/redis";
|
||||||
import { PalleteColor, PrismaClient } from "@prisma/client";
|
import { Logger } from "./lib/Logger";
|
||||||
import { getRedis, client as redisClient } from "./lib/redis";
|
import { ExpressServer } from "./lib/Express";
|
||||||
import Canvas from "./lib/Canvas";
|
import { SocketServer } from "./lib/SocketServer";
|
||||||
import RedisStore from "connect-redis";
|
|
||||||
|
// Validate environment variables
|
||||||
|
|
||||||
if (!process.env.PORT || isNaN(parseInt(process.env.PORT))) {
|
if (!process.env.PORT || isNaN(parseInt(process.env.PORT))) {
|
||||||
console.log("PORT env is not a valid number");
|
Logger.error("PORT env is not a valid number");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRedis().then(() => {
|
if (
|
||||||
console.log("redis connected");
|
!process.env.NODE_ENV ||
|
||||||
});
|
["development", "production"].indexOf(process.env.NODE_ENV) === -1
|
||||||
|
) {
|
||||||
|
Logger.error("NODE_ENV is not valid [development, production]");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const session = expressSession({
|
if (!process.env.SESSION_SECRET) {
|
||||||
secret: "jagoprhaupihuaciohruearp8349jud",
|
Logger.error("SESSION_SECRET is not defined");
|
||||||
resave: false,
|
process.exit(1);
|
||||||
saveUninitialized: false,
|
}
|
||||||
store: new RedisStore({
|
|
||||||
client: redisClient,
|
|
||||||
prefix: "canvas_session:",
|
|
||||||
}),
|
|
||||||
cookie: {
|
|
||||||
sameSite: "none",
|
|
||||||
httpOnly: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const app = express();
|
|
||||||
const server = http.createServer(app);
|
|
||||||
const io = new Server<
|
|
||||||
{
|
|
||||||
place: (
|
|
||||||
data: CPixelPacket,
|
|
||||||
callback: (data: PacketAck<SPixelPacket>) => {}
|
|
||||||
) => void;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user: (user: AuthSession) => void;
|
|
||||||
config: (config: any) => void;
|
|
||||||
pixel: (data: SPixelPacket) => void;
|
|
||||||
canvas: (pixels: string[]) => void;
|
|
||||||
online: (data: { count: number }) => void;
|
|
||||||
}
|
|
||||||
>(server, {
|
|
||||||
cors: {
|
|
||||||
origin: "http://10.1.10.248:5173",
|
|
||||||
credentials: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var PALLETE: PalleteColor[] = [];
|
if (!process.env.REDIS_HOST) {
|
||||||
const PIXEL_TIMEOUT_MS = 1000;
|
Logger.error("REDIS_HOST is not defined");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
prisma.palleteColor
|
if (!process.env.REDIS_SESSION_PREFIX) {
|
||||||
.findMany()
|
Logger.info(
|
||||||
.then((palleteColors) => {
|
"REDIS_SESSION_PREFIX was not defined, defaulting to canvas_session:"
|
||||||
PALLETE = palleteColors;
|
|
||||||
console.log(`Loaded ${palleteColors.length} pallete colors`);
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
|
|
||||||
// on startup, cache current top-level pixels on redis
|
|
||||||
// notify all other shards with pixel updates via redis
|
|
||||||
|
|
||||||
// cacheCanvasToRedis().then(() => {
|
|
||||||
// console.log("canvas is now in redis");
|
|
||||||
// });
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
const sockets = await io.sockets.fetchSockets();
|
|
||||||
for (const socket of sockets) {
|
|
||||||
socket.emit("online", { count: sockets.length });
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
io.engine.use(session);
|
|
||||||
io.on("connection", (socket) => {
|
|
||||||
const user = socket.request.session.user
|
|
||||||
? {
|
|
||||||
sub:
|
|
||||||
socket.request.session.user.user.username +
|
|
||||||
"@" +
|
|
||||||
socket.request.session.user.service.instance.hostname,
|
|
||||||
...socket.request.session.user,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
console.log("connection", socket.request.session.user);
|
|
||||||
|
|
||||||
if (socket.request.session.user)
|
|
||||||
socket.emit("user", socket.request.session.user);
|
|
||||||
|
|
||||||
socket.emit("config", {
|
|
||||||
pallete: {
|
|
||||||
colors: PALLETE,
|
|
||||||
pixel_cooldown: PIXEL_TIMEOUT_MS,
|
|
||||||
},
|
|
||||||
canvas: Canvas.getCanvasConfig(),
|
|
||||||
});
|
|
||||||
|
|
||||||
Canvas.getPixelsArray().then((pixels) => {
|
|
||||||
socket.emit("canvas", pixels);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on(
|
|
||||||
"place",
|
|
||||||
async (
|
|
||||||
{ x, y, color }: CPixelPacket,
|
|
||||||
ack: (data: PacketAck<SPixelPacket>) => {}
|
|
||||||
) => {
|
|
||||||
if (!user) {
|
|
||||||
ack({
|
|
||||||
success: false,
|
|
||||||
error: "no_user",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const puser = await prisma.user.findFirst({ where: { sub: user.sub } });
|
|
||||||
if (puser?.lastPixelTime) {
|
|
||||||
if (
|
|
||||||
puser.lastPixelTime.getTime() + PIXEL_TIMEOUT_MS >
|
|
||||||
new Date().getTime()
|
|
||||||
) {
|
|
||||||
ack({
|
|
||||||
success: false,
|
|
||||||
error: "pixel_cooldown",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const palleteColor = await prisma.palleteColor.findFirst({
|
|
||||||
where: {
|
|
||||||
id: color,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!palleteColor) {
|
|
||||||
ack({
|
|
||||||
success: false,
|
|
||||||
error: "pallete_color_invalid",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Canvas.setPixel(user, x, y, palleteColor.hex);
|
|
||||||
|
|
||||||
ack({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
type: "pixel",
|
|
||||||
_direction: "server->client",
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
socket.broadcast.emit("pixel", {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
color,
|
|
||||||
type: "pixel",
|
|
||||||
_direction: "server->client",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
app.use(session);
|
Redis.connect();
|
||||||
app.use(express.static("../client-next/public"));
|
|
||||||
app.use("/api", APIRoutes);
|
|
||||||
|
|
||||||
server.listen(parseInt(process.env.PORT!), () => {
|
const express = new ExpressServer();
|
||||||
console.log("Listening on " + process.env.PORT);
|
new SocketServer(express.httpServer);
|
||||||
});
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from "./prisma";
|
import { prisma } from "./prisma";
|
||||||
import { getRedis } from "./redis";
|
import { Redis } from "./redis";
|
||||||
|
|
||||||
const redis_keys = {
|
const redis_keys = {
|
||||||
pixelColor: (x: number, y: number) => `CANVAS:PIXELS[${x},${y}]:COLOR`,
|
pixelColor: (x: number, y: number) => `CANVAS:PIXELS[${x},${y}]:COLOR`,
|
||||||
|
@ -24,7 +24,7 @@ class Canvas {
|
||||||
* Latest database pixels -> Redis
|
* Latest database pixels -> Redis
|
||||||
*/
|
*/
|
||||||
async pixelsToRedis() {
|
async pixelsToRedis() {
|
||||||
const redis = await getRedis();
|
const redis = await Redis.getClient();
|
||||||
|
|
||||||
const key = redis_keys.pixelColor;
|
const key = redis_keys.pixelColor;
|
||||||
|
|
||||||
|
@ -52,9 +52,9 @@ class Canvas {
|
||||||
* @returns 1D array of pixel values
|
* @returns 1D array of pixel values
|
||||||
*/
|
*/
|
||||||
async canvasToRedis() {
|
async canvasToRedis() {
|
||||||
const redis = await getRedis();
|
const redis = await Redis.getClient();
|
||||||
|
|
||||||
let pixels: string[] = [];
|
const pixels: string[] = [];
|
||||||
|
|
||||||
for (let x = 0; x < this.CANVAS_SIZE[0]; x++) {
|
for (let x = 0; x < this.CANVAS_SIZE[0]; x++) {
|
||||||
for (let y = 0; y < this.CANVAS_SIZE[1]; y++) {
|
for (let y = 0; y < this.CANVAS_SIZE[1]; y++) {
|
||||||
|
@ -73,11 +73,11 @@ class Canvas {
|
||||||
* force an update at a specific position
|
* force an update at a specific position
|
||||||
*/
|
*/
|
||||||
async updateCanvasRedisAtPos(x: number, y: number) {
|
async updateCanvasRedisAtPos(x: number, y: number) {
|
||||||
const redis = await getRedis();
|
const redis = await Redis.getClient();
|
||||||
|
|
||||||
let pixels: string[] = ((await redis.get(redis_keys.canvas())) || "").split(
|
const pixels: string[] = (
|
||||||
","
|
(await redis.get(redis_keys.canvas())) || ""
|
||||||
);
|
).split(",");
|
||||||
|
|
||||||
pixels[this.CANVAS_SIZE[0] * y + x] =
|
pixels[this.CANVAS_SIZE[0] * y + x] =
|
||||||
(await redis.get(redis_keys.pixelColor(x, y))) || "transparent";
|
(await redis.get(redis_keys.pixelColor(x, y))) || "transparent";
|
||||||
|
@ -86,7 +86,7 @@ class Canvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPixelsArray() {
|
async getPixelsArray() {
|
||||||
const redis = await getRedis();
|
const redis = await Redis.getClient();
|
||||||
|
|
||||||
if (await redis.exists(redis_keys.canvas())) {
|
if (await redis.exists(redis_keys.canvas())) {
|
||||||
const cached = await redis.get(redis_keys.canvas());
|
const cached = await redis.get(redis_keys.canvas());
|
||||||
|
@ -97,7 +97,7 @@ class Canvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPixel(user: { sub: string }, x: number, y: number, hex: string) {
|
async setPixel(user: { sub: string }, x: number, y: number, hex: string) {
|
||||||
const redis = await getRedis();
|
const redis = await Redis.getClient();
|
||||||
|
|
||||||
await prisma.pixel.create({
|
await prisma.pixel.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import http from "node:http";
|
||||||
|
import express, { type Express } from "express";
|
||||||
|
import expressSession from "express-session";
|
||||||
|
import RedisStore from "connect-redis";
|
||||||
|
import { Redis } from "./redis";
|
||||||
|
import APIRoutes from "../api";
|
||||||
|
import { Logger } from "./Logger";
|
||||||
|
|
||||||
|
export const session = expressSession({
|
||||||
|
secret: process.env.SESSION_SECRET,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: new RedisStore({
|
||||||
|
client: Redis.client,
|
||||||
|
prefix: process.env.REDIS_SESSION_PREFIX || "canvas_session:",
|
||||||
|
}),
|
||||||
|
cookie: {
|
||||||
|
sameSite: "none",
|
||||||
|
httpOnly: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export class ExpressServer {
|
||||||
|
app: Express;
|
||||||
|
httpServer: http.Server;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.app = express();
|
||||||
|
this.httpServer = http.createServer(this.app);
|
||||||
|
|
||||||
|
this.app.use(session);
|
||||||
|
this.app.use("/api", APIRoutes);
|
||||||
|
|
||||||
|
this.httpServer.listen(parseInt(process.env.PORT), () => {
|
||||||
|
Logger.info("Listening on :" + process.env.PORT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import winston, { format } from "winston";
|
||||||
|
|
||||||
|
export const Logger = winston.createLogger({
|
||||||
|
level: process.env.LOG_LEVEL || "info",
|
||||||
|
format: format.combine(format.splat(), format.cli()),
|
||||||
|
transports: [new winston.transports.Console()],
|
||||||
|
});
|
|
@ -0,0 +1,165 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.setupOnlineTick();
|
||||||
|
|
||||||
|
this.io.engine.use(session);
|
||||||
|
this.io.on("connection", this.handleConnection.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnection(socket: Socket) {
|
||||||
|
const clientConfig = getClientConfig();
|
||||||
|
const user = this.getUserFromSocket(socket);
|
||||||
|
Logger.debug("Socket connection " + (user ? "@" + user.sub : "No Auth"));
|
||||||
|
|
||||||
|
if (socket.request.session.user) {
|
||||||
|
// inform the client of their session if it exists
|
||||||
|
socket.emit("user", socket.request.session.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit("config", getClientConfig());
|
||||||
|
Canvas.getPixelsArray().then((pixels) => {
|
||||||
|
socket.emit("canvas", pixels);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("place", async (pixel, ack) => {
|
||||||
|
if (!user) {
|
||||||
|
ack({ success: false, error: "no_user" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const puser = await prisma.user.findFirst({ where: { sub: user.sub } });
|
||||||
|
if (puser?.lastPixelTime) {
|
||||||
|
if (
|
||||||
|
puser.lastPixelTime.getTime() + clientConfig.pallete.pixel_cooldown >
|
||||||
|
Date.now()
|
||||||
|
) {
|
||||||
|
ack({
|
||||||
|
success: false,
|
||||||
|
error: "pixel_cooldown",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const palleteColor = await prisma.palleteColor.findFirst({
|
||||||
|
where: {
|
||||||
|
id: pixel.color,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!palleteColor) {
|
||||||
|
ack({
|
||||||
|
success: false,
|
||||||
|
error: "pallete_color_invalid",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Canvas.setPixel(user, pixel.x, pixel.y, palleteColor.hex);
|
||||||
|
|
||||||
|
const newPixel: Pixel = {
|
||||||
|
x: pixel.x,
|
||||||
|
y: pixel.y,
|
||||||
|
color: pixel.color,
|
||||||
|
};
|
||||||
|
ack({
|
||||||
|
success: true,
|
||||||
|
data: newPixel,
|
||||||
|
});
|
||||||
|
socket.broadcast.emit("pixel", newPixel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserFromSocket(socket: Socket) {
|
||||||
|
return socket.request.session.user
|
||||||
|
? {
|
||||||
|
sub:
|
||||||
|
socket.request.session.user.user.username +
|
||||||
|
"@" +
|
||||||
|
socket.request.session.user.service.instance.hostname,
|
||||||
|
...socket.request.session.user,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setup the online people announcement
|
||||||
|
*
|
||||||
|
* this does work with multiple socket.io instances, so this needs to only be executed by one shard
|
||||||
|
*/
|
||||||
|
setupOnlineTick() {
|
||||||
|
setInterval(async () => {
|
||||||
|
const sockets = await this.io.sockets.fetchSockets();
|
||||||
|
for (const socket of sockets) {
|
||||||
|
socket.emit("online", { count: sockets.length });
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,34 @@
|
||||||
import { RedisClientType } from "@redis/client";
|
import { RedisClientType } from "@redis/client";
|
||||||
import { createClient } from "redis";
|
import { createClient } from "redis";
|
||||||
|
import { Logger } from "./Logger";
|
||||||
|
|
||||||
let isConnected = false;
|
class _Redis {
|
||||||
export const client = createClient();
|
isConnected = false;
|
||||||
|
client: RedisClientType;
|
||||||
|
|
||||||
export const getRedis = async () => {
|
constructor() {
|
||||||
if (!isConnected) {
|
this.client = createClient({
|
||||||
await client.connect();
|
url: process.env.REDIS_HOST,
|
||||||
isConnected = true;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
async connect() {
|
||||||
};
|
if (this.isConnected)
|
||||||
|
throw new Error("Attempted to run Redis#connect when already connected");
|
||||||
|
|
||||||
|
await this.client.connect();
|
||||||
|
Logger.info("Connected to Redis");
|
||||||
|
this.isConnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getClient() {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
await this.connect();
|
||||||
|
this.isConnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Redis = new _Redis();
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import type { IncomingMessage } from "http";
|
import type { Session } from "express-session";
|
||||||
import type { Session, SessionData } from "express-session";
|
|
||||||
import type { Socket } from "socket.io";
|
|
||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
import { AuthSession } from "@sc07-canvas/lib/src/net";
|
import { AuthSession } from "@sc07-canvas/lib/src/net";
|
||||||
|
|
||||||
|
@ -16,3 +14,23 @@ declare module "http" {
|
||||||
session: Session & Partial<session.SessionData>;
|
session: Session & Partial<session.SessionData>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
NODE_ENV: "development" | "production";
|
||||||
|
PORT: string;
|
||||||
|
LOG_LEVEL?: string;
|
||||||
|
SESSION_SECRET: string;
|
||||||
|
REDIS_HOST: string;
|
||||||
|
REDIS_SESSION_PREFIX: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is set, enable socket.io CORS to this origin
|
||||||
|
*
|
||||||
|
* Specifically setting CORS origin is required because of use of credentials (cookies)
|
||||||
|
*/
|
||||||
|
CLIENT_ORIGIN?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue