v3: code quality pass, various bug fixes

This commit is contained in:
khannurien
2026-03-23 07:47:49 +00:00
parent d94a319d96
commit fbbbb43258
44 changed files with 1060 additions and 698 deletions

View File

@@ -374,22 +374,32 @@ export async function replaceFileDump(
}
const data = new Uint8Array(await file.arrayBuffer());
await Deno.writeFile(`${DUMPS_DIR}/${dumpId}`, data);
const filePath = `${DUMPS_DIR}/${dumpId}`;
// Read old file contents so we can restore on DB failure
const oldData = await Deno.readFile(filePath).catch(() => null);
await Deno.writeFile(filePath, data);
const now = new Date();
const newSlug = makeSlug(file.name, dumpId);
db.prepare(
`UPDATE dumps SET title = ?, slug = ?, file_name = ?, file_mime = ?, file_size = ?, comment = ?, updated_at = ? WHERE id = ?;`,
).run(
file.name,
newSlug,
file.name,
file.type,
file.size,
comment ?? null,
now.toISOString(),
dumpId,
);
try {
db.prepare(
`UPDATE dumps SET title = ?, slug = ?, file_name = ?, file_mime = ?, file_size = ?, comment = ?, updated_at = ? WHERE id = ?;`,
).run(
file.name,
newSlug,
file.name,
file.type,
file.size,
comment ?? null,
now.toISOString(),
dumpId,
);
} catch (err) {
// Roll back the file to its previous contents on DB failure
if (oldData) await Deno.writeFile(filePath, oldData).catch(() => {});
else await Deno.remove(filePath).catch(() => {});
throw err;
}
if (comment) notifyMentions(dump.userId, comment, "dump", dumpId, file.name);
return {

View File

@@ -2,6 +2,7 @@ import type {
Notification,
NotificationData,
NotificationType,
UserDumpPostedData,
} from "../model/interfaces.ts";
import { APIErrorCode, APIException } from "../model/interfaces.ts";
import { db, isNotificationRow, notificationRowToApi } from "../model/db.ts";
@@ -156,14 +157,53 @@ export function notifyUserFollowersNewDump(
`SELECT follower_id FROM follows WHERE followed_user_id = ?;`,
).all(dumperId) as { follower_id: string }[];
if (followerRows.length === 0) return;
const data: UserDumpPostedData = {
dumperId,
dumperUsername: posterRow.username,
dumpId,
dumpTitle,
};
const dataJson = JSON.stringify(data);
const createdAt = new Date().toISOString();
const sourceKey = `dump:${dumpId}`;
// Batch INSERT all follower notifications in a single statement
const params: (string | number | null)[] = [];
const placeholders: string[] = [];
for (const row of followerRows) {
createNotification(
const id = crypto.randomUUID();
placeholders.push("(?, ?, ?, ?, 0, ?, ?)");
params.push(
id,
row.follower_id,
"user_dump_posted",
{ dumperId, dumperUsername: posterRow.username, dumpId, dumpTitle },
`dump:${dumpId}`,
dataJson,
createdAt,
sourceKey,
);
}
const result = db.prepare(
`INSERT OR IGNORE INTO notifications (id, user_id, type, data, read, created_at, source_key)
VALUES ${placeholders.join(", ")};`,
).run(...params);
if ((result.changes as number) > 0) {
for (const row of followerRows) {
sendToUser(row.follower_id, {
type: "notification_created",
notification: {
userId: row.follower_id,
type: "user_dump_posted",
data,
read: false,
createdAt,
},
});
}
}
}
export function notifyDumpOwnerUpvote(

View File

@@ -95,7 +95,8 @@ export function getPlaylist(
// For public playlists (or when viewed by non-owner), filter out private dumps
const rows = db.prepare(
`SELECT ${dumpCols}
`SELECT ${dumpCols},
(SELECT COUNT(*) FROM comments WHERE dump_id = d.id AND deleted = 0) as comment_count
FROM dumps d
INNER JOIN playlist_dumps pd ON d.id = pd.dump_id
WHERE pd.playlist_id = ?

View File

@@ -80,10 +80,18 @@ export function extractOgTag(
return undefined;
}
function isPrivateHost(hostname: string): boolean {
// Block loopback and RFC-1918 ranges. Note: DNS rebinding is not fully mitigated.
if (hostname === "localhost" || hostname === "::1") return true;
return /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(hostname);
}
export function isValidHttpUrl(raw: string): boolean {
try {
const u = new URL(raw);
return u.protocol === "http:" || u.protocol === "https:";
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
if (isPrivateHost(u.hostname)) return false;
return true;
} catch {
return false;
}

View File

@@ -3,6 +3,7 @@ import type {
Dump,
OnlineUser,
Playlist,
User,
} from "../model/interfaces.ts";
export interface WsClient {
@@ -11,6 +12,7 @@ export interface WsClient {
username?: string;
avatarMime?: string;
avatarVersion?: number;
pongReceived?: boolean;
}
const clients = new Set<WsClient>();
@@ -151,6 +153,12 @@ export function broadcastPlaylistDumpsUpdated(
});
}
export function broadcastUserUpdated(user: Omit<User, "passwordHash">): void {
for (const client of clients) {
send(client.socket, { type: "user_updated", user });
}
}
export function broadcastCommentCreated(comment: Comment): void {
for (const client of clients) {
send(client.socket, { type: "comment_created", comment });
@@ -172,7 +180,11 @@ export function broadcastCommentUpdated(comment: Comment): void {
}
}
// Keepalive: ping all clients every 30s, remove non-responsive ones
export function handleClientPong(client: WsClient): void {
client.pongReceived = true;
}
// Keepalive: ping all clients every 30s, disconnect non-responsive ones
const PING_INTERVAL = 30_000;
setInterval(() => {
@@ -181,7 +193,13 @@ setInterval(() => {
clients.delete(client);
continue;
}
// Disconnect if no pong since last ping (pongReceived starts undefined, skip first cycle)
if (client.pongReceived === false) {
client.socket.close(1001, "Ping timeout");
clients.delete(client);
continue;
}
client.pongReceived = false;
send(client.socket, { type: "ping" });
// Schedule removal if no pong (tracked via heartbeat flag)
}
}, PING_INTERVAL);