chore: apply prettier on the entire project

This commit is contained in:
jubnl
2026-05-25 21:59:42 +02:00
parent c130ed41be
commit 6bcdfbc34b
488 changed files with 82986 additions and 45830 deletions
+219 -71
View File
@@ -1,9 +1,10 @@
import path from 'path';
import fs from 'fs';
import { db, canAccessTrip } from '../db/database';
import { CollabNote, CollabPoll, CollabMessage, TripFile } from '../types';
import { checkSsrf, createPinnedDispatcher } from '../utils/ssrfGuard';
import fs from 'fs';
import path from 'path';
/* ------------------------------------------------------------------ */
/* Internal row types */
/* ------------------------------------------------------------------ */
@@ -61,12 +62,16 @@ export function verifyTripAccess(tripId: string | number, userId: number) {
/* ------------------------------------------------------------------ */
export function loadReactions(messageId: number | string): ReactionRow[] {
return db.prepare(`
return db
.prepare(
`
SELECT r.emoji, r.user_id, u.username
FROM collab_message_reactions r
JOIN users u ON r.user_id = u.id
WHERE r.message_id = ?
`).all(messageId) as ReactionRow[];
`,
)
.all(messageId) as ReactionRow[];
}
export function groupReactions(reactions: ReactionRow[]): GroupedReaction[] {
@@ -78,15 +83,26 @@ export function groupReactions(reactions: ReactionRow[]): GroupedReaction[] {
return Object.entries(map).map(([emoji, users]) => ({ emoji, users, count: users.length }));
}
export function addOrRemoveReaction(messageId: number | string, tripId: number | string, userId: number, emoji: string): { found: boolean; reactions: GroupedReaction[] } {
export function addOrRemoveReaction(
messageId: number | string,
tripId: number | string,
userId: number,
emoji: string,
): { found: boolean; reactions: GroupedReaction[] } {
const msg = db.prepare('SELECT id FROM collab_messages WHERE id = ? AND trip_id = ?').get(messageId, tripId);
if (!msg) return { found: false, reactions: [] };
const existing = db.prepare('SELECT id FROM collab_message_reactions WHERE message_id = ? AND user_id = ? AND emoji = ?').get(messageId, userId, emoji) as { id: number } | undefined;
const existing = db
.prepare('SELECT id FROM collab_message_reactions WHERE message_id = ? AND user_id = ? AND emoji = ?')
.get(messageId, userId, emoji) as { id: number } | undefined;
if (existing) {
db.prepare('DELETE FROM collab_message_reactions WHERE id = ?').run(existing.id);
} else {
db.prepare('INSERT INTO collab_message_reactions (message_id, user_id, emoji) VALUES (?, ?, ?)').run(messageId, userId, emoji);
db.prepare('INSERT INTO collab_message_reactions (message_id, user_id, emoji) VALUES (?, ?, ?)').run(
messageId,
userId,
emoji,
);
}
return { found: true, reactions: groupReactions(loadReactions(messageId)) };
@@ -97,45 +113,84 @@ export function addOrRemoveReaction(messageId: number | string, tripId: number |
/* ------------------------------------------------------------------ */
export function formatNote(note: CollabNote) {
const attachments = db.prepare('SELECT id, filename, original_name, file_size, mime_type FROM trip_files WHERE note_id = ?').all(note.id) as NoteFileRow[];
const attachments = db
.prepare('SELECT id, filename, original_name, file_size, mime_type FROM trip_files WHERE note_id = ?')
.all(note.id) as NoteFileRow[];
return {
...note,
avatar_url: avatarUrl(note),
attachments: attachments.map(a => ({ ...a, url: `/api/trips/${note.trip_id}/files/${a.id}/download` })),
attachments: attachments.map((a) => ({ ...a, url: `/api/trips/${note.trip_id}/files/${a.id}/download` })),
};
}
export function listNotes(tripId: string | number) {
const notes = db.prepare(`
const notes = db
.prepare(
`
SELECT n.*, u.username, u.avatar
FROM collab_notes n
JOIN users u ON n.user_id = u.id
WHERE n.trip_id = ?
ORDER BY n.pinned DESC, n.updated_at DESC
`).all(tripId) as CollabNote[];
`,
)
.all(tripId) as CollabNote[];
return notes.map(formatNote);
}
export function createNote(tripId: string | number, userId: number, data: { title: string; content?: string; category?: string; color?: string; website?: string; pinned?: boolean }) {
export function createNote(
tripId: string | number,
userId: number,
data: { title: string; content?: string; category?: string; color?: string; website?: string; pinned?: boolean },
) {
const pinned = data.pinned ? 1 : 0;
const result = db.prepare(`
const result = db
.prepare(
`
INSERT INTO collab_notes (trip_id, user_id, title, content, category, color, website, pinned)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(tripId, userId, data.title, data.content || null, data.category || 'General', data.color || '#6366f1', data.website || null, pinned);
`,
)
.run(
tripId,
userId,
data.title,
data.content || null,
data.category || 'General',
data.color || '#6366f1',
data.website || null,
pinned,
);
const note = db.prepare(`
const note = db
.prepare(
`
SELECT n.*, u.username, u.avatar FROM collab_notes n JOIN users u ON n.user_id = u.id WHERE n.id = ?
`).get(result.lastInsertRowid) as CollabNote;
`,
)
.get(result.lastInsertRowid) as CollabNote;
return formatNote(note);
}
export function updateNote(tripId: string | number, noteId: string | number, data: { title?: string; content?: string; category?: string; color?: string; pinned?: number | boolean; website?: string }): ReturnType<typeof formatNote> | null {
export function updateNote(
tripId: string | number,
noteId: string | number,
data: {
title?: string;
content?: string;
category?: string;
color?: string;
pinned?: number | boolean;
website?: string;
},
): ReturnType<typeof formatNote> | null {
const existing = db.prepare('SELECT * FROM collab_notes WHERE id = ? AND trip_id = ?').get(noteId, tripId);
if (!existing) return null;
db.prepare(`
db.prepare(
`
UPDATE collab_notes SET
title = COALESCE(?, title),
content = CASE WHEN ? THEN ? ELSE content END,
@@ -145,19 +200,27 @@ export function updateNote(tripId: string | number, noteId: string | number, dat
website = CASE WHEN ? THEN ? ELSE website END,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(
`,
).run(
data.title || null,
data.content !== undefined ? 1 : 0, data.content !== undefined ? data.content : null,
data.content !== undefined ? 1 : 0,
data.content !== undefined ? data.content : null,
data.category || null,
data.color || null,
data.pinned !== undefined ? 1 : null, data.pinned ? 1 : 0,
data.website !== undefined ? 1 : 0, data.website !== undefined ? data.website : null,
noteId
data.pinned !== undefined ? 1 : null,
data.pinned ? 1 : 0,
data.website !== undefined ? 1 : 0,
data.website !== undefined ? data.website : null,
noteId,
);
const note = db.prepare(`
const note = db
.prepare(
`
SELECT n.*, u.username, u.avatar FROM collab_notes n JOIN users u ON n.user_id = u.id WHERE n.id = ?
`).get(noteId) as CollabNote;
`,
)
.get(noteId) as CollabNote;
return formatNote(note);
}
@@ -170,7 +233,11 @@ export function deleteNote(tripId: string | number, noteId: string | number): bo
const noteFiles = db.prepare('SELECT id, filename FROM trip_files WHERE note_id = ?').all(noteId) as NoteFileRow[];
for (const f of noteFiles) {
const filePath = path.join(__dirname, '../../uploads', f.filename);
try { fs.unlinkSync(filePath); } catch { /* ignore */ }
try {
fs.unlinkSync(filePath);
} catch {
/* ignore */
}
}
db.prepare('DELETE FROM trip_files WHERE note_id = ?').run(noteId);
@@ -182,29 +249,43 @@ export function deleteNote(tripId: string | number, noteId: string | number): bo
/* Note files */
/* ------------------------------------------------------------------ */
export function addNoteFile(tripId: string | number, noteId: string | number, file: { filename: string; originalname: string; size: number; mimetype: string }): { file: TripFile & { url: string } } | null {
export function addNoteFile(
tripId: string | number,
noteId: string | number,
file: { filename: string; originalname: string; size: number; mimetype: string },
): { file: TripFile & { url: string } } | null {
const note = db.prepare('SELECT id FROM collab_notes WHERE id = ? AND trip_id = ?').get(noteId, tripId);
if (!note) return null;
const result = db.prepare(
'INSERT INTO trip_files (trip_id, note_id, filename, original_name, file_size, mime_type) VALUES (?, ?, ?, ?, ?, ?)'
).run(tripId, noteId, `files/${file.filename}`, file.originalname, file.size, file.mimetype);
const result = db
.prepare(
'INSERT INTO trip_files (trip_id, note_id, filename, original_name, file_size, mime_type) VALUES (?, ?, ?, ?, ?, ?)',
)
.run(tripId, noteId, `files/${file.filename}`, file.originalname, file.size, file.mimetype);
const saved = db.prepare('SELECT * FROM trip_files WHERE id = ?').get(result.lastInsertRowid) as TripFile;
return { file: { ...saved, url: `/api/trips/${tripId}/files/${saved.id}/download` } };
}
export function getFormattedNoteById(noteId: string | number) {
const note = db.prepare('SELECT n.*, u.username, u.avatar FROM collab_notes n JOIN users u ON n.user_id = u.id WHERE n.id = ?').get(noteId) as CollabNote;
const note = db
.prepare('SELECT n.*, u.username, u.avatar FROM collab_notes n JOIN users u ON n.user_id = u.id WHERE n.id = ?')
.get(noteId) as CollabNote;
return formatNote(note);
}
export function deleteNoteFile(noteId: string | number, fileId: string | number): boolean {
const file = db.prepare('SELECT * FROM trip_files WHERE id = ? AND note_id = ?').get(fileId, noteId) as TripFile | undefined;
const file = db.prepare('SELECT * FROM trip_files WHERE id = ? AND note_id = ?').get(fileId, noteId) as
| TripFile
| undefined;
if (!file) return false;
const filePath = path.join(__dirname, '../../uploads', file.filename);
try { fs.unlinkSync(filePath); } catch { /* ignore */ }
try {
fs.unlinkSync(filePath);
} catch {
/* ignore */
}
db.prepare('DELETE FROM trip_files WHERE id = ?').run(fileId);
return true;
@@ -215,29 +296,43 @@ export function deleteNoteFile(noteId: string | number, fileId: string | number)
/* ------------------------------------------------------------------ */
export function getPollWithVotes(pollId: number | bigint | string) {
const poll = db.prepare(`
const poll = db
.prepare(
`
SELECT p.*, u.username, u.avatar
FROM collab_polls p
JOIN users u ON p.user_id = u.id
WHERE p.id = ?
`).get(pollId) as CollabPoll | undefined;
`,
)
.get(pollId) as CollabPoll | undefined;
if (!poll) return null;
const options: (string | { label: string })[] = JSON.parse(poll.options);
const votes = db.prepare(`
const votes = db
.prepare(
`
SELECT v.option_index, v.user_id, u.username, u.avatar
FROM collab_poll_votes v
JOIN users u ON v.user_id = u.id
WHERE v.poll_id = ?
`).all(pollId) as PollVoteRow[];
`,
)
.all(pollId) as PollVoteRow[];
const formattedOptions = options.map((label: string | { label: string }, idx: number) => ({
label: typeof label === 'string' ? label : label.label || label,
voters: votes
.filter(v => v.option_index === idx)
.map(v => ({ id: v.user_id, user_id: v.user_id, username: v.username, avatar: v.avatar, avatar_url: avatarUrl(v) })),
.filter((v) => v.option_index === idx)
.map((v) => ({
id: v.user_id,
user_id: v.user_id,
username: v.username,
avatar: v.avatar,
avatar_url: avatarUrl(v),
})),
}));
return {
@@ -250,26 +345,45 @@ export function getPollWithVotes(pollId: number | bigint | string) {
}
export function listPolls(tripId: string | number) {
const rows = db.prepare(`
const rows = db
.prepare(
`
SELECT id FROM collab_polls WHERE trip_id = ? ORDER BY created_at DESC
`).all(tripId) as { id: number }[];
`,
)
.all(tripId) as { id: number }[];
return rows.map(row => getPollWithVotes(row.id)).filter(Boolean);
return rows.map((row) => getPollWithVotes(row.id)).filter(Boolean);
}
export function createPoll(tripId: string | number, userId: number, data: { question: string; options: unknown[]; multiple?: boolean; multiple_choice?: boolean; deadline?: string }) {
export function createPoll(
tripId: string | number,
userId: number,
data: { question: string; options: unknown[]; multiple?: boolean; multiple_choice?: boolean; deadline?: string },
) {
const isMultiple = data.multiple || data.multiple_choice;
const result = db.prepare(`
const result = db
.prepare(
`
INSERT INTO collab_polls (trip_id, user_id, question, options, multiple, deadline)
VALUES (?, ?, ?, ?, ?, ?)
`).run(tripId, userId, data.question, JSON.stringify(data.options), isMultiple ? 1 : 0, data.deadline || null);
`,
)
.run(tripId, userId, data.question, JSON.stringify(data.options), isMultiple ? 1 : 0, data.deadline || null);
return getPollWithVotes(result.lastInsertRowid);
}
export function votePoll(tripId: string | number, pollId: string | number, userId: number, optionIndex: number): { error?: string; poll?: ReturnType<typeof getPollWithVotes> } {
const poll = db.prepare('SELECT * FROM collab_polls WHERE id = ? AND trip_id = ?').get(pollId, tripId) as CollabPoll | undefined;
export function votePoll(
tripId: string | number,
pollId: string | number,
userId: number,
optionIndex: number,
): { error?: string; poll?: ReturnType<typeof getPollWithVotes> } {
const poll = db.prepare('SELECT * FROM collab_polls WHERE id = ? AND trip_id = ?').get(pollId, tripId) as
| CollabPoll
| undefined;
if (!poll) return { error: 'not_found' };
if (poll.closed) return { error: 'closed' };
@@ -278,9 +392,9 @@ export function votePoll(tripId: string | number, pollId: string | number, userI
return { error: 'invalid_index' };
}
const existingVote = db.prepare(
'SELECT id FROM collab_poll_votes WHERE poll_id = ? AND user_id = ? AND option_index = ?'
).get(pollId, userId, optionIndex) as { id: number } | undefined;
const existingVote = db
.prepare('SELECT id FROM collab_poll_votes WHERE poll_id = ? AND user_id = ? AND option_index = ?')
.get(pollId, userId, optionIndex) as { id: number } | undefined;
if (existingVote) {
db.prepare('DELETE FROM collab_poll_votes WHERE id = ?').run(existingVote.id);
@@ -288,13 +402,20 @@ export function votePoll(tripId: string | number, pollId: string | number, userI
if (!poll.multiple) {
db.prepare('DELETE FROM collab_poll_votes WHERE poll_id = ? AND user_id = ?').run(pollId, userId);
}
db.prepare('INSERT INTO collab_poll_votes (poll_id, user_id, option_index) VALUES (?, ?, ?)').run(pollId, userId, optionIndex);
db.prepare('INSERT INTO collab_poll_votes (poll_id, user_id, option_index) VALUES (?, ?, ?)').run(
pollId,
userId,
optionIndex,
);
}
return { poll: getPollWithVotes(pollId) };
}
export function closePoll(tripId: string | number, pollId: string | number): ReturnType<typeof getPollWithVotes> | null {
export function closePoll(
tripId: string | number,
pollId: string | number,
): ReturnType<typeof getPollWithVotes> | null {
const poll = db.prepare('SELECT * FROM collab_polls WHERE id = ? AND trip_id = ?').get(pollId, tripId);
if (!poll) return null;
@@ -319,7 +440,9 @@ export function formatMessage(msg: CollabMessage, reactions?: GroupedReaction[])
}
export function countMessages(tripId: string | number): number {
const row = db.prepare('SELECT COUNT(*) as cnt FROM collab_messages WHERE trip_id = ?').get(tripId) as { cnt: number };
const row = db.prepare('SELECT COUNT(*) as cnt FROM collab_messages WHERE trip_id = ?').get(tripId) as {
cnt: number;
};
return row.cnt;
}
@@ -337,40 +460,55 @@ export function listMessages(tripId: string | number, before?: string | number)
`;
const messages = before
? db.prepare(query).all(tripId, before) as CollabMessage[]
: db.prepare(query).all(tripId) as CollabMessage[];
? (db.prepare(query).all(tripId, before) as CollabMessage[])
: (db.prepare(query).all(tripId) as CollabMessage[]);
messages.reverse();
const msgIds = messages.map(m => m.id);
const msgIds = messages.map((m) => m.id);
const reactionsByMsg: Record<number, ReactionRow[]> = {};
if (msgIds.length > 0) {
const allReactions = db.prepare(`
const allReactions = db
.prepare(
`
SELECT r.message_id, r.emoji, r.user_id, u.username
FROM collab_message_reactions r
JOIN users u ON r.user_id = u.id
WHERE r.message_id IN (${msgIds.map(() => '?').join(',')})
`).all(...msgIds) as (ReactionRow & { message_id: number })[];
`,
)
.all(...msgIds) as (ReactionRow & { message_id: number })[];
for (const r of allReactions) {
if (!reactionsByMsg[r.message_id]) reactionsByMsg[r.message_id] = [];
reactionsByMsg[r.message_id].push(r);
}
}
return messages.map(m => formatMessage(m, groupReactions(reactionsByMsg[m.id] || [])));
return messages.map((m) => formatMessage(m, groupReactions(reactionsByMsg[m.id] || [])));
}
export function createMessage(tripId: string | number, userId: number, text: string, replyTo?: number | null): { error?: string; message?: ReturnType<typeof formatMessage> } {
export function createMessage(
tripId: string | number,
userId: number,
text: string,
replyTo?: number | null,
): { error?: string; message?: ReturnType<typeof formatMessage> } {
if (replyTo) {
const replyMsg = db.prepare('SELECT id FROM collab_messages WHERE id = ? AND trip_id = ?').get(replyTo, tripId);
if (!replyMsg) return { error: 'reply_not_found' };
}
const result = db.prepare(`
const result = db
.prepare(
`
INSERT INTO collab_messages (trip_id, user_id, text, reply_to) VALUES (?, ?, ?, ?)
`).run(tripId, userId, text.trim(), replyTo || null);
`,
)
.run(tripId, userId, text.trim(), replyTo || null);
const message = db.prepare(`
const message = db
.prepare(
`
SELECT m.*, u.username, u.avatar,
rm.text AS reply_text, ru.username AS reply_username
FROM collab_messages m
@@ -378,13 +516,21 @@ export function createMessage(tripId: string | number, userId: number, text: str
LEFT JOIN collab_messages rm ON m.reply_to = rm.id
LEFT JOIN users ru ON rm.user_id = ru.id
WHERE m.id = ?
`).get(result.lastInsertRowid) as CollabMessage;
`,
)
.get(result.lastInsertRowid) as CollabMessage;
return { message: formatMessage(message) };
}
export function deleteMessage(tripId: string | number, messageId: string | number, userId: number): { error?: string; username?: string } {
const message = db.prepare('SELECT * FROM collab_messages WHERE id = ? AND trip_id = ?').get(messageId, tripId) as CollabMessage | undefined;
export function deleteMessage(
tripId: string | number,
messageId: string | number,
userId: number,
): { error?: string; username?: string } {
const message = db.prepare('SELECT * FROM collab_messages WHERE id = ? AND trip_id = ?').get(messageId, tripId) as
| CollabMessage
| undefined;
if (!message) return { error: 'not_found' };
if (Number(message.user_id) !== Number(userId)) return { error: 'not_owner' };
@@ -413,7 +559,7 @@ export async function fetchLinkPreview(url: string): Promise<LinkPreviewResult>
const r = await fetch(url, {
redirect: 'error',
signal: controller.signal,
dispatcher: createPinnedDispatcher(ssrf.resolvedIp!),
dispatcher: createPinnedDispatcher(ssrf.resolvedIp),
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NOMAD/1.0; +https://github.com/mauriceboe/NOMAD)' },
} as any);
clearTimeout(timeout);
@@ -421,13 +567,15 @@ export async function fetchLinkPreview(url: string): Promise<LinkPreviewResult>
const html = await r.text();
const get = (prop: string) => {
const m = html.match(new RegExp(`<meta[^>]*property=["']og:${prop}["'][^>]*content=["']([^"']*)["']`, 'i'))
|| html.match(new RegExp(`<meta[^>]*content=["']([^"']*)["'][^>]*property=["']og:${prop}["']`, 'i'));
const m =
html.match(new RegExp(`<meta[^>]*property=["']og:${prop}["'][^>]*content=["']([^"']*)["']`, 'i')) ||
html.match(new RegExp(`<meta[^>]*content=["']([^"']*)["'][^>]*property=["']og:${prop}["']`, 'i'));
return m ? m[1] : null;
};
const titleTag = html.match(/<title[^>]*>([^<]*)<\/title>/i);
const descMeta = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["']/i)
|| html.match(/<meta[^>]*content=["']([^"']*)["'][^>]*name=["']description["']/i);
const descMeta =
html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["']/i) ||
html.match(/<meta[^>]*content=["']([^"']*)["'][^>]*name=["']description["']/i);
return {
title: get('title') || (titleTag ? titleTag[1].trim() : null),