import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' import { executeSoapCommand } from './soap' export interface RecruitRewardView { id: number required_friends: number reward_name: string item_id: number item_quantity: number item_link: string icon_class: string claimed: boolean } /** IDs de cuentas reclutadas por `accountId` (con su IP). */ async function getRecruitedAccounts(accountId: number): Promise<{ id: number; last_ip: string | null }[]> { const [rows] = await db(DB.auth).query( 'SELECT id, last_ip FROM account WHERE recruiter = ?', [accountId], ) return rows.map((r) => ({ id: r.id, last_ip: r.last_ip ?? null })) } /** Nº de personajes de amigos reclutados que han alcanzado el nivel 80. */ export async function getRecruitedLevel80Count(accountId: number): Promise { const recruited = await getRecruitedAccounts(accountId) if (recruited.length === 0) return 0 const ids = recruited.map((r) => r.id) const [rows] = await db(DB.characters).query( 'SELECT COUNT(*) AS n FROM characters WHERE level = 80 AND account IN (?)', [ids], ) return Number(rows[0]?.n ?? 0) } /** Estado de reclutamiento de la cuenta: si fue reclutada y cuántos amigos ha reclutado. */ export async function getRecruitStats(accountId: number): Promise<{ isRecruited: boolean; recruitedCount: number }> { let isRecruited = false let recruitedCount = 0 try { const [r] = await db(DB.auth).query('SELECT recruiter FROM account WHERE id = ?', [accountId]) isRecruited = Boolean(r[0]?.recruiter) const [c] = await db(DB.auth).query('SELECT COUNT(*) AS n FROM account WHERE recruiter = ?', [accountId]) recruitedCount = Number(c[0]?.n ?? 0) } catch { /* ignore */ } return { isRecruited, recruitedCount } } /** * Cuentas reclutadas válidas: tienen un personaje de nivel 80 y NO comparten IP * con el reclutador (anti-abuso). Devuelve hasta `requiredCount` IDs. */ async function getValidRecruitsAtLevel80(accountId: number, requiredCount: number): Promise { const recruited = await getRecruitedAccounts(accountId) if (recruited.length === 0) return [] const ipById = new Map(recruited.map((r) => [r.id, r.last_ip])) const ids = recruited.map((r) => r.id) const [ipRows] = await db(DB.auth).query('SELECT last_ip FROM account WHERE id = ?', [accountId]) const userIp = ipRows[0]?.last_ip ?? null const [rows] = await db(DB.characters).query( 'SELECT DISTINCT account FROM characters WHERE level = 80 AND account IN (?)', [ids], ) const atLevel80 = rows.map((r) => Number(r.account)) // Excluir los que comparten IP con el reclutador. return atLevel80.filter((acc) => ipById.get(acc) !== userIp).slice(0, requiredCount) } /** Todas las recompensas con marca de si esta cuenta ya las reclamó. */ export async function getRecruitRewards(accountId: number | null): Promise { const [rows] = await db(DB.default).query( 'SELECT id, required_friends, reward_name, item_id, item_quantity, item_link, icon_class FROM recruitreward ORDER BY required_friends, id', ) let claimedIds = new Set() if (accountId) { const [claimed] = await db(DB.default).query( 'SELECT recruit_reward_id FROM claimedreward WHERE account_id = ?', [accountId], ) claimedIds = new Set(claimed.map((c) => Number(c.recruit_reward_id))) } return rows.map((r) => ({ id: r.id, required_friends: r.required_friends, reward_name: r.reward_name, item_id: r.item_id, item_quantity: r.item_quantity, item_link: r.item_link, icon_class: r.icon_class, claimed: claimedIds.has(Number(r.id)), })) } /** ¿El personaje pertenece a la cuenta? */ async function characterBelongsToAccount(accountId: number, name: string): Promise { const [rows] = await db(DB.characters).query( 'SELECT 1 FROM characters WHERE name = ? AND account = ? LIMIT 1', [name, accountId], ) return Boolean(rows[0]) } export interface ClaimResult { success: boolean message: string // clave i18n de RecruitClaim } /** Reclama una recompensa de reclutamiento: valida requisitos, envía el item por SOAP y lo registra. */ export async function claimRecruitReward( accountId: number, username: string, rewardId: number, characterName: string, ip: string, ): Promise { const [rewardRows] = await db(DB.default).query( 'SELECT id, required_friends, reward_name, item_id, item_quantity FROM recruitreward WHERE id = ?', [rewardId], ) const reward = rewardRows[0] if (!reward) return { success: false, message: 'rewardNotFound' } if (!(await characterBelongsToAccount(accountId, characterName))) { return { success: false, message: 'invalidCharacter' } } const [already] = await db(DB.default).query( 'SELECT 1 FROM claimedreward WHERE account_id = ? AND recruit_reward_id = ? LIMIT 1', [accountId, rewardId], ) if (already[0]) return { success: false, message: 'alreadyClaimed' } const valid = await getValidRecruitsAtLevel80(accountId, reward.required_friends) if (valid.length < reward.required_friends) return { success: false, message: 'requirementsNotMet' } const command = `.send items ${characterName} 'Recompensa de Recluta Amigo' 'Tu amigo alcanzó el Nivel 80' ${reward.item_id}:${reward.item_quantity}` const response = await executeSoapCommand(command) if (!response || !response.includes('Mail sent to')) { return { success: false, message: 'deliveryError' } } await db(DB.default).query( 'INSERT INTO claimedreward (account_id, username, recruit_reward_id, character_name, claimed_at, ip_address) VALUES (?, ?, ?, ?, NOW(), ?)', [accountId, username, rewardId, characterName, ip.slice(0, 45)], ) return { success: true, message: 'claimSuccess' } }