feat: implement keepalive mechanism for IRC connection and enhance match actions with role-based permissions

This commit is contained in:
2026-02-28 21:17:05 +00:00
parent 04d772d8ad
commit 593797c751
5 changed files with 146 additions and 120 deletions

View File

@@ -5,8 +5,10 @@ import { playPickedMap, closeLobby, forceStartGame } from '$lib/server/match/orc
import { getLobby } from '$lib/server/bancho/client';
import { error, redirect } from '@sveltejs/kit';
import { getBeatmap } from '$lib/server/osu/api';
import { hasRole } from '$lib/server/permissions';
import type { PageServerLoad, Actions } from './$types';
import { eq } from 'drizzle-orm';
import type { MatchConfig } from '$lib/server/match/types';
export const load: PageServerLoad = async ({ params, locals }) => {
@@ -41,7 +43,9 @@ export const load: PageServerLoad = async ({ params, locals }) => {
);
}
return { match: m, beatmapCache, userId: locals.user.id };
const isStaff = hasRole(locals.user.role, 'referee');
return { match: m, beatmapCache, userId: locals.user.id, isStaff };
};
export const actions: Actions = {
@@ -51,20 +55,14 @@ export const actions: Actions = {
const lobby = getLobby(params.id);
if (!lobby) return { error: 'No active IRC lobby for this match' };
const m = await getMatchFull(params.id);
const invited: string[] = [];
for (const p of m.participants) {
for (const pl of p.players) {
const u = await db.query.user.findFirst({ where: eq(user.id, pl.userId) });
if (u?.name) {
await lobby.invite(u.name);
invited.push(u.name);
}
}
// Only invite the currently logged-in user
const u = await db.query.user.findFirst({ where: eq(user.id, locals.user.id) });
if (u?.name) {
await lobby.invite(u.name);
return { reinvited: [u.name] };
}
return { reinvited: invited };
return { error: 'Could not find your username' };
},
roll: async ({ params, locals }) => {
@@ -132,6 +130,28 @@ export const actions: Actions = {
if (!expectedPicker) return { error: 'Cannot determine picker' };
// ── Verify the logged-in user is on the expected picker's team ──
const isOnPickerTeam = expectedPicker.players.some(
(pl) => pl.userId === locals.user!.id
);
// Allow staff to pick on behalf of anyone
const isStaff = hasRole(locals.user!.role, 'referee');
if (!isOnPickerTeam && !isStaff) {
return { error: "It's not your turn to pick" };
}
// ── Tiebreaker restriction ──
// TB maps can only be picked when both teams are at match point (e.g. 2-2 in BO5)
const config = m.config as MatchConfig;
const winsNeeded = Math.ceil(config.bestOf / 2);
const slot = m.mappool?.slots?.find((s) => s.id === slotId);
if (slot?.category === 'TB') {
const allAtMatchPoint = m.participants.every((p) => p.score === winsNeeded - 1);
if (!allAtMatchPoint) {
return { error: 'Tiebreaker can only be picked when both teams are at match point' };
}
}
try {
const game = await pickMap(params.id, expectedPicker.id, slotId);
@@ -148,12 +168,20 @@ export const actions: Actions = {
forceStart: async ({ params, locals }) => {
if (!locals.user) redirect(302, '/');
// Only staff can force start
if (!hasRole(locals.user.role, 'referee')) {
return { error: 'Only referees and admins can force start' };
}
await forceStartGame(params.id);
return { started: true };
},
cancel: async ({ params, locals }) => {
if (!locals.user) redirect(302, '/');
// Only staff can cancel matches
if (!hasRole(locals.user.role, 'referee')) {
return { error: 'Only referees and admins can cancel matches' };
}
await cancelMatch(params.id);
await closeLobby(params.id);