import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' export interface ForumInfo { id: number category_id: number name: string description: string icon: string | null topic_count: number post_count: number last_topic_id: number | null last_topic_name: string | null last_topic_poster: string | null } export interface Category { id: number name: string forums: ForumInfo[] } export interface Topic { id: number name: string poster: string created: string | null sticky: boolean locked: boolean views: number } export interface Post { id: number poster: string poster_id: number | null text: string time: string | null edited: boolean deleted: boolean } export interface TopicFull { id: number name: string forum_id: number poster_id: number | null locked: boolean sticky: boolean deleted: 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). */ export async function getForumIndex(locale = 'es'): Promise { const en = locale === 'en' try { const [cats] = await db(DB.web).query( 'SELECT id, name, name_en FROM forum_categories ORDER BY `order`, id', ) const [forums] = await db(DB.web).query( `SELECT f.id, f.category_id, f.name, f.name_en, f.description, f.description_en, f.icon, (SELECT COUNT(*) FROM forum_topics t WHERE t.forum_id = f.id AND t.deleted = 0) AS topic_count, (SELECT COUNT(*) FROM forum_posts p JOIN forum_topics t ON p.topic_id = t.id WHERE t.forum_id = f.id AND p.deleted = 0 AND t.deleted = 0) AS post_count, (SELECT t2.id FROM forum_topics t2 WHERE t2.forum_id = f.id AND t2.deleted = 0 ORDER BY t2.updated_at DESC, t2.id DESC LIMIT 1) AS last_topic_id, (SELECT t3.name FROM forum_topics t3 WHERE t3.forum_id = f.id AND t3.deleted = 0 ORDER BY t3.updated_at DESC, t3.id DESC LIMIT 1) AS last_topic_name, (SELECT t4.poster FROM forum_topics t4 WHERE t4.forum_id = f.id AND t4.deleted = 0 ORDER BY t4.updated_at DESC, t4.id DESC LIMIT 1) AS last_topic_poster FROM forums f WHERE f.visibility = 1 AND f.deleted = 0 ORDER BY f.\`order\`, f.id`, ) const byCat = new Map() for (const f of forums) { const info = f as unknown as ForumInfo & { name_en?: string | null; description_en?: string | null } info.name = en && info.name_en ? info.name_en : info.name info.description = en && info.description_en ? info.description_en : info.description if (!byCat.has(info.category_id)) byCat.set(info.category_id, []) byCat.get(info.category_id)!.push(info) } return cats .map((c) => ({ id: c.id, name: en && c.name_en ? c.name_en : c.name, forums: byCat.get(c.id) ?? [] })) .filter((c) => c.forums.length > 0) } catch { return [] } } /** Lista de foros visibles para el desplegable de «mover tema». */ export async function listForumsForMove(): Promise<{ id: number; name: string }[]> { try { const [rows] = await db(DB.web).query( 'SELECT id, name FROM forums WHERE visibility = 1 AND deleted = 0 ORDER BY `order`, id', ) return rows.map((r) => ({ id: r.id, name: r.name })) } catch { return [] } } export async function getForum(id: number, locale = 'es'): Promise<{ name: string; description: string } | null> { const en = locale === 'en' try { const [rows] = await db(DB.web).query( 'SELECT name, name_en, description, description_en FROM forums WHERE id = ? AND visibility = 1 AND deleted = 0', [id], ) if (!rows[0]) return null return { name: en && rows[0].name_en ? rows[0].name_en : rows[0].name, description: en && rows[0].description_en ? rows[0].description_en : rows[0].description, } } catch { return null } } export async function countTopics(forumId: number): Promise { try { const [rows] = await db(DB.web).query( 'SELECT COUNT(*) AS n FROM forum_topics WHERE forum_id = ? AND deleted = 0', [forumId], ) return Number(rows[0]?.n ?? 0) } catch { return 0 } } export async function getTopics(forumId: number, offset = 0, limit = 20): Promise { try { const [rows] = await db(DB.web).query( 'SELECT id, name, poster, created, sticky, locked, views FROM forum_topics WHERE forum_id = ? AND deleted = 0 ORDER BY sticky DESC, updated_at DESC, id DESC LIMIT ? OFFSET ?', [forumId, limit, offset], ) return rows.map((r) => ({ id: r.id, name: r.name, poster: r.poster, created: r.created ? new Date(r.created).toISOString() : null, sticky: r.sticky === 1, locked: r.locked === 1, views: r.views ?? 0, })) } catch { return [] } } export async function getTopic(id: number, includeDeleted = false): Promise { try { const where = includeDeleted ? 'id = ?' : 'id = ? AND deleted = 0' const [rows] = await db(DB.web).query( `SELECT id, name, forum_id, poster_id, locked, sticky, deleted FROM forum_topics WHERE ${where}`, [id], ) 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, deleted: r.deleted === 1, } : null } catch { return null } } export async function countPosts(topicId: number, includeDeleted = false): Promise { try { const del = includeDeleted ? '' : 'AND deleted = 0' const [rows] = await db(DB.web).query( `SELECT COUNT(*) AS n FROM forum_posts WHERE topic_id = ? ${del}`, [topicId], ) return Number(rows[0]?.n ?? 0) } catch { return 0 } } export async function getPosts(topicId: number, offset = 0, limit = 15, includeDeleted = false): Promise { try { const del = includeDeleted ? '' : 'AND deleted = 0' const [rows] = await db(DB.web).query( `SELECT id, poster, poster_id, text, time, created_at, updated_at, deleted FROM forum_posts WHERE topic_id = ? ${del} ORDER BY time ASC, id ASC LIMIT ? OFFSET ?`, [topicId, limit, offset], ) 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, ), deleted: r.deleted === 1, })) } catch { return [] } } export interface ForumProfile { username: string postCount: number topicCount: number joindate: string | null gmlevel: number status: 'admin' | 'gm' | null recentTopics: { id: number; name: string; forum_id: number }[] } /** Perfil público de un usuario del foro (contadores + antigüedad + rango). */ export async function getForumProfile(username: string): Promise { const profile: ForumProfile = { username, postCount: 0, topicCount: 0, joindate: null, gmlevel: 0, status: null, recentTopics: [], } try { const [pc] = await db(DB.web).query( 'SELECT COUNT(*) AS n FROM forum_posts WHERE poster = ? AND deleted = 0', [username], ) profile.postCount = Number(pc[0]?.n ?? 0) const [tc] = await db(DB.web).query( 'SELECT COUNT(*) AS n FROM forum_topics WHERE poster = ? AND deleted = 0', [username], ) profile.topicCount = Number(tc[0]?.n ?? 0) const [rt] = await db(DB.web).query( 'SELECT id, name, forum_id FROM forum_topics WHERE poster = ? AND deleted = 0 ORDER BY updated_at DESC, id DESC LIMIT 5', [username], ) profile.recentTopics = rt.map((r) => ({ id: r.id, name: r.name, forum_id: r.forum_id })) } catch { /* tablas de foro no pobladas */ } try { const [acc] = await db(DB.auth).query( 'SELECT id, joindate FROM account WHERE username = ?', [username], ) if (acc[0]) { profile.joindate = acc[0].joindate ? new Date(acc[0].joindate).toISOString() : null const [ga] = await db(DB.auth).query( 'SELECT MAX(SecurityLevel) AS lvl FROM account_access WHERE AccountID = ?', [acc[0].id], ) profile.gmlevel = ga[0]?.lvl != null ? Number(ga[0].lvl) : 0 } } catch { /* account_access puede no existir */ } if (profile.gmlevel >= 16) profile.status = 'admin' else if (profile.gmlevel >= 1) profile.status = 'gm' return profile } /** Nº de temas que coinciden con la búsqueda (foros visibles). */ export async function countSearchTopics(query: string): Promise { const like = `%${query}%` try { const [rows] = await db(DB.web).query( `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 { const like = `%${query}%` try { const [rows] = await db(DB.web).query( `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 [] } }