v1 review pass: fixed some minor bugs

This commit is contained in:
khannurien
2026-03-16 11:08:39 +00:00
parent e88fed4e98
commit 867e64cb5b
37 changed files with 1228 additions and 400 deletions

View File

@@ -69,7 +69,17 @@ export async function createUrlDump(
richContent ? JSON.stringify(richContent) : null,
);
const dump: Dump = { id: dumpId, kind: "url", title, comment: request.comment, userId, createdAt, url: request.url, richContent, voteCount: 0 };
const dump: Dump = {
id: dumpId,
kind: "url",
title,
comment: request.comment,
userId,
createdAt,
url: request.url,
richContent,
voteCount: 0,
};
broadcastNewDump(dump);
return dump;
}
@@ -87,7 +97,11 @@ export async function createFileDump(
);
}
if (file.size > MAX_FILE_SIZE) {
throw new APIException(APIErrorCode.BAD_REQUEST, 400, "File too large (max 50 MB)");
throw new APIException(
APIErrorCode.BAD_REQUEST,
400,
"File too large (max 50 MB)",
);
}
const dumpId = crypto.randomUUID();
@@ -153,7 +167,11 @@ export function listDumps(): Dump[] {
).all();
if (!rows || !rows.every(isDumpRow)) {
throw new APIException(APIErrorCode.SERVER_ERROR, 500, "Malformed dump data");
throw new APIException(
APIErrorCode.SERVER_ERROR,
500,
"Malformed dump data",
);
}
return rows.map(dumpRowToApi);
@@ -167,7 +185,12 @@ export async function updateDump(
// File dumps: only comment is editable
if (dump.kind === "file") {
const updatedDump = { ...dump, comment: "comment" in request ? (request.comment ?? undefined) : dump.comment };
const updatedDump = {
...dump,
comment: "comment" in request
? (request.comment ?? undefined)
: dump.comment,
};
db.prepare(`UPDATE dumps SET comment = ? WHERE id = ?;`)
.run(updatedDump.comment ?? null, dumpId);
return updatedDump;
@@ -190,7 +213,9 @@ export async function updateDump(
const updatedDump: Dump = {
...dump,
title,
comment: "comment" in request ? (request.comment ?? undefined) : dump.comment,
comment: "comment" in request
? (request.comment ?? undefined)
: dump.comment,
url: newUrl,
richContent,
};
@@ -213,10 +238,18 @@ export async function replaceFileDump(
comment: string | undefined,
): Promise<Dump> {
if (!isAllowedMime(file.type)) {
throw new APIException(APIErrorCode.BAD_REQUEST, 400, `File type '${file.type}' is not allowed`);
throw new APIException(
APIErrorCode.BAD_REQUEST,
400,
`File type '${file.type}' is not allowed`,
);
}
if (file.size > MAX_FILE_SIZE) {
throw new APIException(APIErrorCode.BAD_REQUEST, 400, "File too large (max 50 MB)");
throw new APIException(
APIErrorCode.BAD_REQUEST,
400,
"File too large (max 50 MB)",
);
}
const dump = getDump(dumpId);
@@ -231,7 +264,14 @@ export async function replaceFileDump(
`UPDATE dumps SET title = ?, file_name = ?, file_mime = ?, file_size = ?, comment = ? WHERE id = ?;`,
).run(file.name, file.name, file.type, file.size, comment ?? null, dumpId);
return { ...dump, title: file.name, fileName: file.name, fileMime: file.type, fileSize: file.size, comment };
return {
...dump,
title: file.name,
fileName: file.name,
fileMime: file.type,
fileSize: file.size,
comment,
};
}
export function getDumpsByUser(userId: string): Dump[] {
@@ -239,7 +279,11 @@ export function getDumpsByUser(userId: string): Dump[] {
`SELECT ${SELECT_COLS} FROM dumps WHERE user_id = ? ORDER BY created_at DESC;`,
).all(userId);
if (!rows.every(isDumpRow)) {
throw new APIException(APIErrorCode.SERVER_ERROR, 500, "Malformed dump data");
throw new APIException(
APIErrorCode.SERVER_ERROR,
500,
"Malformed dump data",
);
}
return rows.map(dumpRowToApi);
}
@@ -253,7 +297,11 @@ export function getVotedDumpsByUser(userId: string): Dump[] {
ORDER BY v.created_at DESC;`,
).all(userId);
if (!rows.every(isDumpRow)) {
throw new APIException(APIErrorCode.SERVER_ERROR, 500, "Malformed dump data");
throw new APIException(
APIErrorCode.SERVER_ERROR,
500,
"Malformed dump data",
);
}
return rows.map(dumpRowToApi);
}

View File

@@ -29,6 +29,13 @@ export const youtubeProvider: RichContentProvider = {
// oembed failed — thumbnail still works
}
return { type: "youtube", siteName: "YouTube", url, videoId, title, thumbnailUrl };
return {
type: "youtube",
siteName: "YouTube",
url,
videoId,
title,
thumbnailUrl,
};
},
};

View File

@@ -33,8 +33,10 @@ export async function fetchWithTimeout(
return await fetch(url, {
signal: controller.signal,
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"Accept":
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
},
});
@@ -51,7 +53,10 @@ function decodeHtmlEntities(str: string): string {
.replace(/&quot;/gi, '"')
.replace(/&apos;/gi, "'")
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
.replace(
/&#x([0-9a-f]+);/gi,
(_, hex) => String.fromCodePoint(parseInt(hex, 16)),
);
}
export function extractOgTag(

View File

@@ -18,7 +18,11 @@ export function castVote(dumpId: string, userId: string): number {
} catch (err) {
db.exec("ROLLBACK;");
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
throw new APIException(APIErrorCode.VALIDATION_ERROR, 409, "Already voted");
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
409,
"Already voted",
);
}
throw err;
}

View File

@@ -65,15 +65,26 @@ export function broadcastDumpDeleted(dumpId: string): void {
}
}
export function broadcastVoteUpdate(dumpId: string, voteCount: number): void {
export function broadcastVoteUpdate(
dumpId: string,
voteCount: number,
voterId: string,
action: "cast" | "remove",
): void {
for (const client of clients) {
send(client.socket, { type: "votes_update", dumpId, voteCount });
send(client.socket, {
type: "votes_update",
dumpId,
voteCount,
voterId,
action,
});
}
}
// Keepalive: ping all clients every 30s, remove non-responsive ones
const PING_INTERVAL = 30_000;
const PONG_TIMEOUT = 5_000;
const _PONG_TIMEOUT = 5_000;
setInterval(() => {
for (const client of clients) {