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",
|
||||
"@nextui-org/react": "^2.2.9",
|
||||
"@sc07-canvas/lib": "^1.0.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"framer-motion": "^11.0.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Socket, io } from "socket.io-client";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import {
|
||||
AuthSession,
|
||||
ClientConfig,
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
} from "../types";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import { AuthSession } from "@sc07-canvas/lib/src/net";
|
||||
} from "@sc07-canvas/lib/src/net";
|
||||
|
||||
export interface INetworkEvents {
|
||||
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 & {
|
||||
type: "place";
|
||||
// socket.io
|
||||
|
||||
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;
|
||||
y: number;
|
||||
color: number;
|
||||
};
|
||||
|
||||
export type SCanvasPacket = ServerPacket & {
|
||||
type: "canvas";
|
||||
pixels: string[];
|
||||
export type PalleteColor = {
|
||||
id: number;
|
||||
name: string;
|
||||
hex: string;
|
||||
};
|
||||
|
||||
export type SPixelPacket = ServerPacket & {
|
||||
type: "pixel";
|
||||
x: number;
|
||||
y: number;
|
||||
color: number;
|
||||
export type CanvasConfig = {
|
||||
size: [number, number];
|
||||
zoom: number;
|
||||
};
|
||||
|
||||
export type SUserPacket = ServerPacket & {
|
||||
type: "user";
|
||||
user: AuthSession;
|
||||
export type ClientConfig = {
|
||||
pallete: {
|
||||
colors: PalleteColor[];
|
||||
pixel_cooldown: number;
|
||||
};
|
||||
canvas: CanvasConfig;
|
||||
};
|
||||
|
||||
export type Packet = {
|
||||
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> =
|
||||
export type PacketAck<T> =
|
||||
| {
|
||||
success: true;
|
||||
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",
|
||||
"main": "./build/index.js",
|
||||
"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": [],
|
||||
"author": "",
|
||||
|
@ -12,7 +13,10 @@
|
|||
"@tsconfig/recommended": "^1.0.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express-session": "^1.17.7",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^3.0.1",
|
||||
"prisma": "^5.3.1",
|
||||
|
@ -27,6 +31,7 @@
|
|||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"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_SECRET = "secret";
|
||||
|
||||
app.get("/me", (req, res) => {
|
||||
res.json(req.session);
|
||||
});
|
||||
|
||||
app.get("/login", (req, res) => {
|
||||
const params = new URLSearchParams();
|
||||
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
|
||||
import "./types";
|
||||
import { prisma } from "./lib/prisma";
|
||||
import { PalleteColor, PrismaClient } from "@prisma/client";
|
||||
import { getRedis, client as redisClient } from "./lib/redis";
|
||||
import Canvas from "./lib/Canvas";
|
||||
import RedisStore from "connect-redis";
|
||||
import { Redis } from "./lib/redis";
|
||||
import { Logger } from "./lib/Logger";
|
||||
import { ExpressServer } from "./lib/Express";
|
||||
import { SocketServer } from "./lib/SocketServer";
|
||||
|
||||
// Validate environment variables
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
getRedis().then(() => {
|
||||
console.log("redis connected");
|
||||
});
|
||||
if (
|
||||
!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({
|
||||
secret: "jagoprhaupihuaciohruearp8349jud",
|
||||
resave: false,
|
||||
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,
|
||||
},
|
||||
});
|
||||
if (!process.env.SESSION_SECRET) {
|
||||
Logger.error("SESSION_SECRET is not defined");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var PALLETE: PalleteColor[] = [];
|
||||
const PIXEL_TIMEOUT_MS = 1000;
|
||||
if (!process.env.REDIS_HOST) {
|
||||
Logger.error("REDIS_HOST is not defined");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
prisma.palleteColor
|
||||
.findMany()
|
||||
.then((palleteColors) => {
|
||||
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",
|
||||
});
|
||||
}
|
||||
if (!process.env.REDIS_SESSION_PREFIX) {
|
||||
Logger.info(
|
||||
"REDIS_SESSION_PREFIX was not defined, defaulting to canvas_session:"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
app.use(session);
|
||||
app.use(express.static("../client-next/public"));
|
||||
app.use("/api", APIRoutes);
|
||||
Redis.connect();
|
||||
|
||||
server.listen(parseInt(process.env.PORT!), () => {
|
||||
console.log("Listening on " + process.env.PORT);
|
||||
});
|
||||
const express = new ExpressServer();
|
||||
new SocketServer(express.httpServer);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { prisma } from "./prisma";
|
||||
import { getRedis } from "./redis";
|
||||
import { Redis } from "./redis";
|
||||
|
||||
const redis_keys = {
|
||||
pixelColor: (x: number, y: number) => `CANVAS:PIXELS[${x},${y}]:COLOR`,
|
||||
|
@ -24,7 +24,7 @@ class Canvas {
|
|||
* Latest database pixels -> Redis
|
||||
*/
|
||||
async pixelsToRedis() {
|
||||
const redis = await getRedis();
|
||||
const redis = await Redis.getClient();
|
||||
|
||||
const key = redis_keys.pixelColor;
|
||||
|
||||
|
@ -52,9 +52,9 @@ class Canvas {
|
|||
* @returns 1D array of pixel values
|
||||
*/
|
||||
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 y = 0; y < this.CANVAS_SIZE[1]; y++) {
|
||||
|
@ -73,11 +73,11 @@ class Canvas {
|
|||
* force an update at a specific position
|
||||
*/
|
||||
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] =
|
||||
(await redis.get(redis_keys.pixelColor(x, y))) || "transparent";
|
||||
|
@ -86,7 +86,7 @@ class Canvas {
|
|||
}
|
||||
|
||||
async getPixelsArray() {
|
||||
const redis = await getRedis();
|
||||
const redis = await Redis.getClient();
|
||||
|
||||
if (await redis.exists(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) {
|
||||
const redis = await getRedis();
|
||||
const redis = await Redis.getClient();
|
||||
|
||||
await prisma.pixel.create({
|
||||
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 { createClient } from "redis";
|
||||
import { Logger } from "./Logger";
|
||||
|
||||
let isConnected = false;
|
||||
export const client = createClient();
|
||||
class _Redis {
|
||||
isConnected = false;
|
||||
client: RedisClientType;
|
||||
|
||||
export const getRedis = async () => {
|
||||
if (!isConnected) {
|
||||
await client.connect();
|
||||
isConnected = true;
|
||||
constructor() {
|
||||
this.client = createClient({
|
||||
url: process.env.REDIS_HOST,
|
||||
});
|
||||
}
|
||||
|
||||
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, SessionData } from "express-session";
|
||||
import type { Socket } from "socket.io";
|
||||
import type { Session } from "express-session";
|
||||
import session from "express-session";
|
||||
import { AuthSession } from "@sc07-canvas/lib/src/net";
|
||||
|
||||
|
@ -16,3 +14,23 @@ declare module "http" {
|
|||
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