import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' import { executeSoapCommand } from './soap' import { getGameCharacters } from './characters' /** * Recuperar ítems (/restore-items): usa el sistema de recuperación de AzerothCore * por SOAP. `.item restore list ` lista los ítems borrados recuperables (el * core aplica las condiciones: calidad Raro+, equipable, se liga al recoger, * borrado < 7 días). `.item restore ` restaura uno. Cada * restauración cuesta **100 PD**. La consulta tiene cooldown de 8 h por personaje. */ export const RESTORE_ITEM_PD = 100 const SEARCH_COOLDOWN_MS = 8 * 60 * 60 * 1000 const SAFE_NAME = /^[A-Za-z]{1,12}$/ export interface DeletedItem { recoverId: number itemId: number name: string count: number } /** Parsea la salida de `.item restore list` (líneas "Recover id: X | Item: NAME (ID) | Count: N"). */ export function parseRecoverList(soap: string): DeletedItem[] { const items: DeletedItem[] = [] const re = /Recover id:\s*(\d+)\s*\|\s*Item:\s*(.+?)\s*\((\d+)\)\s*\|\s*Count:\s*(\d+)/g let m: RegExpExecArray | null while ((m = re.exec(soap)) !== null) { items.push({ recoverId: Number(m[1]), name: m[2].trim(), itemId: Number(m[3]), count: Number(m[4]) }) } return items } async function ownsCharacter(accountId: number, character: string): Promise { if (!SAFE_NAME.test(character)) return false const chars = await getGameCharacters(accountId) return chars.some((c) => c.name === character) } export interface ListResult { success: boolean error?: string minutes?: number items?: DeletedItem[] } /** Lista los ítems borrados recuperables de un personaje (cooldown 8 h). */ export async function listDeletedItems(accountId: number, character: string): Promise { if (!(await ownsCharacter(accountId, character))) return { success: false, error: 'characterNotOwned' } // Cooldown de 8 h por personaje. try { const [last] = await db(DB.default).query( 'SELECT used_at FROM home_itemsearchhistory WHERE character_name = ? ORDER BY used_at DESC LIMIT 1', [character], ) if (last[0]) { const diff = Date.now() - new Date(last[0].used_at).getTime() if (diff < SEARCH_COOLDOWN_MS) { return { success: false, error: 'cooldown', minutes: Math.ceil((SEARCH_COOLDOWN_MS - diff) / 60000) } } } } catch { /* ignore */ } const soap = await executeSoapCommand(`.item restore list ${character}`) if (soap === null) return { success: false, error: 'soapError' } await db(DB.default) .query('INSERT INTO home_itemsearchhistory (character_name, used_at) VALUES (?, NOW())', [character]) .catch(() => {}) return { success: true, items: parseRecoverList(soap) } } export interface RestoreResult { success: boolean error?: string name?: string } /** Restaura un ítem borrado (cuesta 100 PD; reembolsa si el SOAP falla). */ export async function restoreItem(accountId: number, character: string, recoverId: number): Promise { if (!(await ownsCharacter(accountId, character))) return { success: false, error: 'characterNotOwned' } if (!Number.isInteger(recoverId) || recoverId <= 0) return { success: false, error: 'invalidRequest' } // Cobra 100 PD de forma atómica. let charged = false try { const [res] = await db(DB.default).query( 'UPDATE home_api_points SET dp = dp - ? WHERE accountID = ? AND dp >= ?', [RESTORE_ITEM_PD, accountId, RESTORE_ITEM_PD], ) // @ts-expect-error affectedRows charged = res.affectedRows === 1 } catch { return { success: false, error: 'genericError' } } if (!charged) return { success: false, error: 'insufficientPoints' } const soap = await executeSoapCommand(`.item restore ${recoverId} ${character}`) if (soap === null) { await db(DB.default) .query('UPDATE home_api_points SET dp = dp + ? WHERE accountID = ?', [RESTORE_ITEM_PD, accountId]) .catch(() => {}) return { success: false, error: 'soapError' } } return { success: true } }