ws handler added
This commit is contained in:
289
src/ws-handler.ts
Normal file
289
src/ws-handler.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import type { ServerWebSocket } from "bun";
|
||||||
|
import {
|
||||||
|
rooms, wsToPlayer,
|
||||||
|
Room, Player,
|
||||||
|
generateId, defaultSettings, freshBuzzerState, sanitize,
|
||||||
|
getPublicRoom, broadcastToRoom, sendToMod,
|
||||||
|
} from "./rooms";
|
||||||
|
|
||||||
|
type WS = ServerWebSocket<unknown>;
|
||||||
|
|
||||||
|
function send(ws: WS, msg: object) {
|
||||||
|
try { ws.send(JSON.stringify(msg)); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function err(ws: WS, message: string) {
|
||||||
|
send(ws, { type: "error", message });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleMessage(ws: WS, raw: string) {
|
||||||
|
let msg: any;
|
||||||
|
try { msg = JSON.parse(raw); } catch { return; }
|
||||||
|
|
||||||
|
const type: string = msg?.type ?? "";
|
||||||
|
|
||||||
|
// ── CREATE ROOM ──────────────────────────────────────────────────────────
|
||||||
|
if (type === "create_room") {
|
||||||
|
const roomId = generateId(6);
|
||||||
|
const modSecret = generateId(24);
|
||||||
|
const room: Room = {
|
||||||
|
id: roomId,
|
||||||
|
moderatorSecret: modSecret,
|
||||||
|
modWs: ws,
|
||||||
|
settings: defaultSettings(),
|
||||||
|
players: new Map(),
|
||||||
|
buzzerState: freshBuzzerState(),
|
||||||
|
locked: false,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
rooms.set(roomId, room);
|
||||||
|
send(ws, { type: "room_created", roomId, modSecret, room: getPublicRoom(room) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MOD REJOIN ────────────────────────────────────────────────────────────
|
||||||
|
if (type === "mod_rejoin") {
|
||||||
|
const { roomId, modSecret } = msg;
|
||||||
|
const room = rooms.get(sanitize(roomId, 12));
|
||||||
|
if (!room || room.moderatorSecret !== sanitize(modSecret, 32)) {
|
||||||
|
err(ws, "Invalid moderator credentials"); return;
|
||||||
|
}
|
||||||
|
room.modWs = ws;
|
||||||
|
send(ws, { type: "mod_joined", room: getPublicRoom(room) });
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) }, ws);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JOIN ROOM (player) ────────────────────────────────────────────────────
|
||||||
|
if (type === "join_room") {
|
||||||
|
const roomId = sanitize(msg.roomId ?? "", 12);
|
||||||
|
const room = rooms.get(roomId);
|
||||||
|
if (!room) { err(ws, "Room not found"); return; }
|
||||||
|
if (room.locked) { err(ws, "Room is locked"); return; }
|
||||||
|
|
||||||
|
const name = sanitize(msg.playerName ?? "Player", 24) || "Player";
|
||||||
|
const existingId = sanitize(msg.playerId ?? "", 12);
|
||||||
|
|
||||||
|
// Block anyone trying to present the mod secret as a player
|
||||||
|
if (msg.modSecret) { err(ws, "Unauthorized"); return; }
|
||||||
|
|
||||||
|
let player: Player | undefined;
|
||||||
|
if (existingId && room.players.has(existingId)) {
|
||||||
|
player = room.players.get(existingId)!;
|
||||||
|
player.ws = ws;
|
||||||
|
player.isConnected = true;
|
||||||
|
player.name = name;
|
||||||
|
} else {
|
||||||
|
const playerId = generateId();
|
||||||
|
player = {
|
||||||
|
id: playerId, name,
|
||||||
|
teamIndex: null, score: 0,
|
||||||
|
ws, isConnected: true,
|
||||||
|
joinedAt: Date.now(),
|
||||||
|
};
|
||||||
|
room.players.set(playerId, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
wsToPlayer.set(ws, { roomId, playerId: player.id });
|
||||||
|
send(ws, { type: "joined", playerId: player.id, room: getPublicRoom(room) });
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) }, ws);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BUZZ ──────────────────────────────────────────────────────────────────
|
||||||
|
if (type === "buzz") {
|
||||||
|
const ctx = wsToPlayer.get(ws);
|
||||||
|
if (!ctx) { err(ws, "Not a player"); return; }
|
||||||
|
const room = rooms.get(ctx.roomId);
|
||||||
|
if (!room) return;
|
||||||
|
const bz = room.buzzerState;
|
||||||
|
|
||||||
|
if (!bz.roundOpen) { send(ws, { type: "buzz_rejected", reason: "Round not open" }); return; }
|
||||||
|
if (bz.buzzOrder.includes(ctx.playerId) && !room.settings.allowRebuzz) {
|
||||||
|
send(ws, { type: "buzz_rejected", reason: "Already buzzed" }); return;
|
||||||
|
}
|
||||||
|
if (room.settings.buzzerLockout && bz.buzzOrder.length > 0) {
|
||||||
|
send(ws, { type: "buzz_rejected", reason: "Buzzer locked out" }); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bz.buzzOrder.push(ctx.playerId);
|
||||||
|
bz.buzzTimes.set(ctx.playerId, Date.now());
|
||||||
|
bz.active = true;
|
||||||
|
|
||||||
|
const player = room.players.get(ctx.playerId)!;
|
||||||
|
const publicOrder = room.settings.showOrder ? bz.buzzOrder : [bz.buzzOrder[0]];
|
||||||
|
|
||||||
|
broadcastToRoom(room, {
|
||||||
|
type: "buzz_event",
|
||||||
|
playerId: ctx.playerId,
|
||||||
|
playerName: player.name,
|
||||||
|
teamIndex: player.teamIndex,
|
||||||
|
buzzOrder: publicOrder,
|
||||||
|
room: getPublicRoom(room),
|
||||||
|
});
|
||||||
|
// mod gets full order + timestamps always
|
||||||
|
sendToMod(room, {
|
||||||
|
type: "buzz_event",
|
||||||
|
playerId: ctx.playerId,
|
||||||
|
playerName: player.name,
|
||||||
|
teamIndex: player.teamIndex,
|
||||||
|
buzzOrder: bz.buzzOrder,
|
||||||
|
buzzTimes: Object.fromEntries(bz.buzzTimes),
|
||||||
|
room: getPublicRoom(room),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MOD-ONLY FROM HERE ────────────────────────────────────────────────────
|
||||||
|
// find room where this ws is the mod
|
||||||
|
let room: Room | undefined;
|
||||||
|
for (const r of rooms.values()) {
|
||||||
|
if (r.modWs === ws) { room = r; break; }
|
||||||
|
}
|
||||||
|
if (!room) { err(ws, "Not authorized"); return; }
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "open_round": {
|
||||||
|
room.buzzerState = freshBuzzerState();
|
||||||
|
room.buzzerState.roundOpen = true;
|
||||||
|
broadcastToRoom(room, { type: "round_open", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "round_open", room: getPublicRoom(room) });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "close_round": {
|
||||||
|
room.buzzerState.roundOpen = false;
|
||||||
|
broadcastToRoom(room, { type: "round_closed", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "round_closed", room: getPublicRoom(room) });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "reset_buzzer": {
|
||||||
|
room.buzzerState = freshBuzzerState();
|
||||||
|
broadcastToRoom(room, { type: "buzzer_reset", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "buzzer_reset", room: getPublicRoom(room) });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "update_settings": {
|
||||||
|
const s = msg.settings ?? {};
|
||||||
|
const st = room.settings;
|
||||||
|
if (typeof s.teamMode === "boolean") st.teamMode = s.teamMode;
|
||||||
|
if (typeof s.numTeams === "number") st.numTeams = Math.min(8, Math.max(2, s.numTeams | 0));
|
||||||
|
if (Array.isArray(s.teamNames)) st.teamNames = s.teamNames.slice(0, 8).map((n: unknown) => sanitize(n, 32));
|
||||||
|
if (typeof s.allowRebuzz === "boolean") st.allowRebuzz = s.allowRebuzz;
|
||||||
|
if (typeof s.buzzerLockout === "boolean") st.buzzerLockout = s.buzzerLockout;
|
||||||
|
if (typeof s.showOrder === "boolean") st.showOrder = s.showOrder;
|
||||||
|
if (typeof s.pointsCorrect === "number") st.pointsCorrect = Math.min(10000, Math.max(0, s.pointsCorrect | 0));
|
||||||
|
if (typeof s.pointsIncorrect === "number") st.pointsIncorrect = Math.min(10000, Math.max(0, s.pointsIncorrect | 0));
|
||||||
|
if (typeof s.pointsNeg === "boolean") st.pointsNeg = s.pointsNeg;
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "settings_updated", room: getPublicRoom(room) });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "assign_team": {
|
||||||
|
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||||
|
if (p) {
|
||||||
|
p.teamIndex = typeof msg.teamIndex === "number" ? Math.max(0, Math.min(7, msg.teamIndex | 0)) : null;
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "adjust_score": {
|
||||||
|
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||||
|
if (p && typeof msg.delta === "number") {
|
||||||
|
p.score = Math.max(-99999, Math.min(99999, p.score + (msg.delta | 0)));
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "set_score": {
|
||||||
|
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||||
|
if (p && typeof msg.score === "number") {
|
||||||
|
p.score = Math.max(-99999, Math.min(99999, msg.score | 0));
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mark_correct": {
|
||||||
|
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||||
|
if (p) {
|
||||||
|
p.score += room.settings.pointsCorrect;
|
||||||
|
room.buzzerState = freshBuzzerState();
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mark_incorrect": {
|
||||||
|
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||||
|
if (p && room.settings.pointsNeg) {
|
||||||
|
p.score -= room.settings.pointsIncorrect;
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "kick_player": {
|
||||||
|
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||||
|
if (p) {
|
||||||
|
if (p.ws) { try { p.ws.send(JSON.stringify({ type: "kicked" })); } catch {} }
|
||||||
|
room.players.delete(p.id);
|
||||||
|
wsToPlayer.delete(p.ws!);
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "lock_room": {
|
||||||
|
room.locked = msg.locked === true;
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "reset_scores": {
|
||||||
|
for (const p of room.players.values()) p.score = 0;
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "reset_teams": {
|
||||||
|
for (const p of room.players.values()) p.teamIndex = null;
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "end_room": {
|
||||||
|
broadcastToRoom(room, { type: "room_ended" });
|
||||||
|
for (const p of room.players.values()) {
|
||||||
|
if (p.ws) wsToPlayer.delete(p.ws);
|
||||||
|
}
|
||||||
|
room.modWs = null;
|
||||||
|
rooms.delete(room.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleClose(ws: WS) {
|
||||||
|
// mod disconnect?
|
||||||
|
for (const room of rooms.values()) {
|
||||||
|
if (room.modWs === ws) {
|
||||||
|
room.modWs = null;
|
||||||
|
broadcastToRoom(room, { type: "mod_disconnected" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// player disconnect
|
||||||
|
const ctx = wsToPlayer.get(ws);
|
||||||
|
if (ctx) {
|
||||||
|
wsToPlayer.delete(ws);
|
||||||
|
const room = rooms.get(ctx.roomId);
|
||||||
|
if (room) {
|
||||||
|
const p = room.players.get(ctx.playerId);
|
||||||
|
if (p) { p.isConnected = false; p.ws = null; }
|
||||||
|
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
sendToMod(room, { type: "room_update", room: getPublicRoom(room) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user