Foro: edición/borrado de posts, moderación de temas y búsqueda
Portado desde forum/db.py + forum/permissions.py de Django: - lib/forum-perm.ts: forumIsModerator (gmlevel >= FORUM_MOD_GMLEVEL) y canEditPost (autor o moderador). - Edición y borrado lógico de posts (autor o mod), respetando tema bloqueado, con validación de longitud mínima (plainLength). - Moderación de temas (solo mod): bloquear/desbloquear, fijar/no fijar, borrar. - Búsqueda de temas por título/contenido (searchTopics/countSearchTopics) con página /forum/search y buscador en el índice. - API: PATCH/DELETE /api/forum/post, POST /api/forum/moderate (401/403 correctos). - UI: PostActions, TopicModBar, ForumSearchBox; marca "editado" y "[Fijado]". - i18n: 22 claves nuevas en es/en. Verificado: build OK, /forum y /forum/search 200, APIs 401 sin sesión. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+84
-4
@@ -33,8 +33,28 @@ export interface Topic {
|
||||
export interface Post {
|
||||
id: number
|
||||
poster: string
|
||||
poster_id: number | null
|
||||
text: string
|
||||
time: string | null
|
||||
edited: boolean
|
||||
}
|
||||
|
||||
export interface TopicFull {
|
||||
id: number
|
||||
name: string
|
||||
forum_id: number
|
||||
poster_id: number | null
|
||||
locked: boolean
|
||||
sticky: boolean
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
id: number
|
||||
name: string
|
||||
poster: string
|
||||
forum_id: number
|
||||
forum_name: string
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
/** Índice del foro: categorías con sus foros visibles (+ contadores y último tema). */
|
||||
@@ -104,13 +124,23 @@ export async function getTopics(forumId: number): Promise<Topic[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTopic(id: number): Promise<{ name: string; locked: boolean } | null> {
|
||||
export async function getTopic(id: number): Promise<TopicFull | null> {
|
||||
try {
|
||||
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
||||
'SELECT name, locked FROM forum_topics WHERE id = ? AND deleted = 0',
|
||||
'SELECT id, name, forum_id, poster_id, locked, sticky FROM forum_topics WHERE id = ? AND deleted = 0',
|
||||
[id],
|
||||
)
|
||||
return rows[0] ? { name: rows[0].name, locked: rows[0].locked === 1 } : null
|
||||
const r = rows[0]
|
||||
return r
|
||||
? {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
forum_id: r.forum_id,
|
||||
poster_id: r.poster_id ?? null,
|
||||
locked: r.locked === 1,
|
||||
sticky: r.sticky === 1,
|
||||
}
|
||||
: null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
@@ -119,14 +149,64 @@ export async function getTopic(id: number): Promise<{ name: string; locked: bool
|
||||
export async function getPosts(topicId: number): Promise<Post[]> {
|
||||
try {
|
||||
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
||||
'SELECT id, poster, text, time FROM forum_posts WHERE topic_id = ? AND deleted = 0 ORDER BY time ASC, id ASC',
|
||||
'SELECT id, poster, poster_id, text, time, created_at, updated_at FROM forum_posts WHERE topic_id = ? AND deleted = 0 ORDER BY time ASC, id ASC',
|
||||
[topicId],
|
||||
)
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
poster: r.poster,
|
||||
poster_id: r.poster_id ?? null,
|
||||
text: r.text,
|
||||
time: r.time ? new Date(r.time).toISOString() : null,
|
||||
// editado si updated_at es posterior (más de 2s) al alta del post
|
||||
edited: Boolean(
|
||||
r.updated_at && r.created_at && new Date(r.updated_at).getTime() - new Date(r.created_at).getTime() > 2000,
|
||||
),
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** Nº de temas que coinciden con la búsqueda (foros visibles). */
|
||||
export async function countSearchTopics(query: string): Promise<number> {
|
||||
const like = `%${query}%`
|
||||
try {
|
||||
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
||||
`SELECT COUNT(DISTINCT t.id) AS n
|
||||
FROM forum_topics t
|
||||
JOIN forums f ON f.id = t.forum_id AND f.visibility = 1 AND f.deleted = 0
|
||||
LEFT JOIN forum_posts p ON p.topic_id = t.id AND p.deleted = 0
|
||||
WHERE t.deleted = 0 AND (t.name LIKE ? OR p.text LIKE ?)`,
|
||||
[like, like],
|
||||
)
|
||||
return Number(rows[0]?.n ?? 0)
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/** Busca temas por título o contenido de sus posts (solo foros visibles). */
|
||||
export async function searchTopics(query: string, offset = 0, limit = 20): Promise<SearchResult[]> {
|
||||
const like = `%${query}%`
|
||||
try {
|
||||
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
||||
`SELECT DISTINCT t.id, t.name, t.poster, t.forum_id, t.updated_at, f.name AS forum_name
|
||||
FROM forum_topics t
|
||||
JOIN forums f ON f.id = t.forum_id AND f.visibility = 1 AND f.deleted = 0
|
||||
LEFT JOIN forum_posts p ON p.topic_id = t.id AND p.deleted = 0
|
||||
WHERE t.deleted = 0 AND (t.name LIKE ? OR p.text LIKE ?)
|
||||
ORDER BY t.updated_at DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[like, like, limit, offset],
|
||||
)
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
poster: r.poster,
|
||||
forum_id: r.forum_id,
|
||||
forum_name: r.forum_name,
|
||||
updated_at: r.updated_at ? new Date(r.updated_at).toISOString() : null,
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user