Files
NightSpire/web-next/lib/guild.ts
T
Inna b9803adeb9 web-next: rediseño de security-token, recover, vote-points + rename-guild
- /security-token: diseño completo del original (info + advertencia + NOTA 7 días),
  fecha en formato HH:MM:SS DD-MM-YYYY, botón "Token enviado", enlace a /recover.
- /recover: 4 opciones (contraseña por usuario, nombre de cuenta, token de seguridad
  y enlace de activación por correo); recuperación de token nueva; Turnstile por envío.
- /vote-points: caja de info con las 7 secciones Q&A + panel "Sitios de votación" con
  tarjetas inline-div (imagen, PV, último voto), botón refrescar; abre el sitio y acredita
  PV al volver. lib/vote.ts + getVoteSitesForAccount.
- /rename-guild: renombra una hermandad de la que la cuenta es Maestro por 1000 PD +
  token de seguridad; SOAP .guild rename con comillas; reembolsa los PD si SOAP falla.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:06:49 +00:00

104 lines
3.7 KiB
TypeScript

import type { RowDataPacket, ResultSetHeader } from 'mysql2'
import { db, DB } from './db'
import { checkSecurityToken } from './security-token'
import { executeSoapCommand } from './soap'
/**
* Renombrar hermandad: el usuario elige una hermandad de la que sea Maestro
* (guild_member.rank = 0) y le pone un nombre nuevo. Cuesta 1000 PD y requiere
* el token de seguridad. El renombrado se aplica por SOAP (.guild rename).
*/
export const GUILD_RENAME_PD = 1000
/** Nombre de hermandad: A-Za-z y espacio, máx. 24 (regla del juego / diseño). */
const NAME_RE = /^[A-Za-z ]{1,24}$/
export interface GmGuild {
guildid: number
name: string
}
/** Hermandades de las que la cuenta tiene un personaje Maestro (rank 0). */
export async function getGuildMasterGuilds(accountId: number): Promise<GmGuild[]> {
if (!accountId) return []
try {
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
`SELECT g.guildid, g.name
FROM guild g
JOIN guild_member gm ON g.guildid = gm.guildid
JOIN characters c ON c.guid = gm.guid
WHERE c.account = ? AND gm.rank = 0
ORDER BY g.name`,
[accountId],
)
return rows.map((r) => ({ guildid: Number(r.guildid), name: r.name }))
} catch {
return []
}
}
export interface Result {
success: boolean
error?: string
cost?: number
newName?: string
}
export async function renameGuild(params: {
accountId: number
guildId: number
newName: string
token: string
}): Promise<Result> {
const { accountId, guildId, token } = params
const newName = (params.newName || '').trim()
if (!accountId || !guildId || !newName || !token) return { success: false, error: 'missingFields' }
if (!NAME_RE.test(newName)) return { success: false, error: 'invalidName' }
if (!(await checkSecurityToken(accountId, token))) return { success: false, error: 'invalidToken' }
// La hermandad debe ser una de las que la cuenta dirige (Maestro).
const guilds = await getGuildMasterGuilds(accountId)
const guild = guilds.find((g) => g.guildid === guildId)
if (!guild) return { success: false, error: 'notGuildMaster' }
const oldName = guild.name
if (/["\r\n]/.test(oldName)) return { success: false, error: 'genericError' } // nombre antiguo no seguro para SOAP
// El nuevo nombre no puede coincidir con otra hermandad existente.
try {
const [dup] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT guildid FROM guild WHERE name = ? AND guildid <> ? LIMIT 1',
[newName, guildId],
)
if (dup[0]) return { success: false, error: 'nameTaken' }
} catch {
return { success: false, error: 'genericError' }
}
// Cobra 1000 PD de forma atómica (condicional sobre el saldo).
let charged = false
try {
const [res] = await db(DB.default).query<ResultSetHeader>(
'UPDATE home_api_points SET dp = dp - ? WHERE accountID = ? AND dp >= ?',
[GUILD_RENAME_PD, accountId, GUILD_RENAME_PD],
)
charged = res.affectedRows === 1
} catch {
return { success: false, error: 'genericError' }
}
if (!charged) return { success: false, error: 'insufficientPoints' }
// Aplica el renombrado en el servidor por SOAP (convención del proyecto:
// respuesta no nula = aplicado, igual que soapFulfill en paid-services). Si el
// worldserver está caído, devuelve los PD cobrados.
const soap = await executeSoapCommand(`.guild rename "${oldName}" "${newName}"`)
if (soap === null) {
await db(DB.default)
.query('UPDATE home_api_points SET dp = dp + ? WHERE accountID = ?', [GUILD_RENAME_PD, accountId])
.catch(() => {})
return { success: false, error: 'soapError' }
}
return { success: true, cost: GUILD_RENAME_PD, newName }
}