ni yo se que hice xd
This commit is contained in:
@@ -1,71 +1,143 @@
|
||||
import {Guild, Invite, User} from "discord.js";
|
||||
import { Guild, Invite, User, GuildMember } from "discord.js";
|
||||
|
||||
/**
|
||||
* Lista de variables válidas del sistema (sin llaves {})
|
||||
* Registro central de variables -> resolutores
|
||||
* Cada clave es el token que aparecerá en el texto (sin llaves),
|
||||
* y su valor es una función que recibe el contexto y devuelve el string a insertar.
|
||||
*/
|
||||
export const VALID_VARIABLES = [
|
||||
'user.name', 'user.id', 'user.mention', 'user.avatar',
|
||||
'user.pointsAll', 'user.pointsWeekly', 'user.pointsMonthly',
|
||||
'guild.name', 'guild.icon',
|
||||
'invite.name', 'invite.icon'
|
||||
];
|
||||
type VarCtx = {
|
||||
user?: User | GuildMember;
|
||||
guild?: Guild;
|
||||
stats?: any;
|
||||
invite?: Invite;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validar si una URL es válida o contiene variables del sistema
|
||||
* @param url - La URL o texto a validar
|
||||
* @returns boolean - true si es válida
|
||||
*/
|
||||
export function isValidUrlOrVariable(url: string): boolean {
|
||||
if (!url) return false;
|
||||
type VarResolver = (ctx: VarCtx) => string | Promise<string>;
|
||||
|
||||
// Verificar si el texto contiene variables válidas
|
||||
const hasValidVariables = VALID_VARIABLES.some(variable => url.includes(variable));
|
||||
if (hasValidVariables) return true;
|
||||
|
||||
// Si no tiene variables, validar como URL normal
|
||||
// Helpers seguros para leer datos de usuario/miembro y guild/invite
|
||||
const getUserId = (u?: User | GuildMember) => (u as any)?.id || (u as any)?.user?.id || "";
|
||||
const getUsername = (u?: User | GuildMember) => (u as any)?.username || (u as any)?.user?.username || "";
|
||||
const getAvatar = (u?: User | GuildMember) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return url.startsWith('http://') || url.startsWith('https://');
|
||||
const fn = (u as any)?.displayAvatarURL || (u as any)?.user?.displayAvatarURL;
|
||||
return typeof fn === 'function' ? fn.call((u as any)?.user ?? u, { forceStatic: false }) : "";
|
||||
} catch { return ""; }
|
||||
};
|
||||
const getGuildIcon = (g?: Guild) => {
|
||||
try { return g?.iconURL({ forceStatic: false }) ?? ""; } catch { return ""; }
|
||||
};
|
||||
|
||||
// Construye datos de invite similares a la versión previa
|
||||
const getInviteObject = (invite?: Invite) => invite?.guild ? {
|
||||
name: invite.guild.name,
|
||||
icon: invite.guild.icon ? `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` : ''
|
||||
} : null;
|
||||
|
||||
export const VARIABLES: Record<string, VarResolver> = {
|
||||
// USER INFO
|
||||
'user.name': ({ user }) => getUsername(user),
|
||||
'user.id': ({ user }) => getUserId(user),
|
||||
'user.mention': ({ user }) => {
|
||||
const id = getUserId(user);
|
||||
return id ? `<@${id}>` : '';
|
||||
},
|
||||
'user.avatar': ({ user }) => getAvatar(user),
|
||||
|
||||
// USER STATS
|
||||
'user.pointsAll': ({ stats }) => stats?.totalPoints?.toString?.() ?? '0',
|
||||
'user.pointsWeekly': ({ stats }) => stats?.weeklyPoints?.toString?.() ?? '0',
|
||||
'user.pointsMonthly': ({ stats }) => stats?.monthlyPoints?.toString?.() ?? '0',
|
||||
|
||||
// GUILD INFO
|
||||
'guild.name': ({ guild }) => guild?.name ?? '',
|
||||
'guild.icon': ({ guild }) => getGuildIcon(guild),
|
||||
|
||||
// INVITE INFO
|
||||
'invite.name': ({ invite }) => getInviteObject(invite)?.name ?? '',
|
||||
'invite.icon': ({ invite }) => getInviteObject(invite)?.icon ?? ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Lista de variables válidas del sistema (derivada del registro)
|
||||
* Exportada por compatibilidad y para UI.
|
||||
*/
|
||||
// @ts-ignore
|
||||
export const VALID_VARIABLES: string[] = Object.freeze(Object.keys(VARIABLES));
|
||||
|
||||
/** Devuelve la lista actual de variables (no congelada) */
|
||||
export function listVariables(): string[] {
|
||||
return Object.keys(VARIABLES);
|
||||
}
|
||||
|
||||
/** Escapa una cadena para uso literal dentro de una RegExp */
|
||||
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
/**
|
||||
* Validar si un texto es una URL válida o contiene variables del sistema
|
||||
* Mantiene la semántica previa: true si contiene cualquier token válido o si es http/https válido.
|
||||
*/
|
||||
export function isValidUrlOrVariable(text: string): boolean {
|
||||
if (!text) return false;
|
||||
|
||||
// ¿Contiene alguna variable?
|
||||
if (VALID_VARIABLES.some(v => text.includes(v))) return true;
|
||||
|
||||
// ¿Es URL http/https válida?
|
||||
try {
|
||||
const u = new URL(text);
|
||||
return u.protocol === 'http:' || u.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
export async function replaceVars(text: string, user: User | undefined, guild: Guild | undefined, stats?: any, invite: Invite | undefined): Promise<string> {
|
||||
if(!text) return '';
|
||||
/**
|
||||
* Reemplaza variables en un texto usando el registro de VARIABLES.
|
||||
* Compatible con llamadas existentes: acepta User o GuildMember en el primer parámetro (históricamente llamado "user").
|
||||
*/
|
||||
export async function replaceVars(
|
||||
text: string,
|
||||
userOrMember: User | GuildMember | undefined,
|
||||
guild: Guild | undefined,
|
||||
stats?: any,
|
||||
invite?: Invite
|
||||
): Promise<string> {
|
||||
if (!text) return '';
|
||||
|
||||
// Crear inviteObject solo si invite existe y tiene guild
|
||||
const inviteObject = invite?.guild ? {
|
||||
name: invite.guild.name,
|
||||
icon: invite.guild.icon ? `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` : ''
|
||||
} : null;
|
||||
const ctx: VarCtx = { user: userOrMember, guild, stats, invite };
|
||||
|
||||
return text
|
||||
/**
|
||||
* USER INFO
|
||||
*/
|
||||
.replace(/(user\.name)/g, user?.username ?? '')
|
||||
.replace(/(user\.id)/g, user?.id ?? '')
|
||||
.replace(/(user\.mention)/g, user ? `<@${user.id}>` : '')
|
||||
.replace(/(user\.avatar)/g, user?.displayAvatarURL({ forceStatic: false }) ?? '')
|
||||
// Construimos una única RegExp que contenga todas las claves (sin anchors, para coincidir en cualquier parte)
|
||||
const keys = Object.keys(VARIABLES);
|
||||
if (keys.length === 0) return text;
|
||||
// Ordenar por longitud descendente para evitar falsas coincidencias de prefijos (defensivo)
|
||||
const keysEscaped = keys.sort((a, b) => b.length - a.length).map(escapeRegex);
|
||||
const pattern = new RegExp(`(${keysEscaped.join('|')})`, 'g');
|
||||
|
||||
/**
|
||||
* USER STATS
|
||||
*/
|
||||
.replace(/(user\.pointsAll)/g, stats?.totalPoints?.toString() ?? '0')
|
||||
.replace(/(user\.pointsWeekly)/g, stats?.weeklyPoints?.toString() ?? '0')
|
||||
.replace(/(user\.pointsMonthly)/g, stats?.monthlyPoints?.toString() ?? '0')
|
||||
// Reemplazo asíncrono
|
||||
const parts: (string | Promise<string>)[] = [];
|
||||
let lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = pattern.exec(text)) !== null) {
|
||||
const matchStart = m.index;
|
||||
if (matchStart > lastIndex) parts.push(text.slice(lastIndex, matchStart));
|
||||
const token = m[1];
|
||||
const resolver = VARIABLES[token];
|
||||
if (resolver) {
|
||||
try {
|
||||
const value = resolver(ctx);
|
||||
parts.push(Promise.resolve(value).then(v => (v ?? '').toString()));
|
||||
} catch {
|
||||
parts.push('');
|
||||
}
|
||||
} else {
|
||||
// No debería ocurrir, pero añadimos el literal por seguridad
|
||||
parts.push(token);
|
||||
}
|
||||
lastIndex = pattern.lastIndex;
|
||||
}
|
||||
if (lastIndex < text.length) parts.push(text.slice(lastIndex));
|
||||
|
||||
/**
|
||||
* GUILD INFO
|
||||
*/
|
||||
.replace(/(guild\.name)/g, guild?.name ?? '')
|
||||
.replace(/(guild\.icon)/g, guild?.iconURL({ forceStatic: false }) ?? '')
|
||||
|
||||
/**
|
||||
* INVITE INFO
|
||||
*/
|
||||
.replace(/(invite\.name)/g, inviteObject?.name ?? "")
|
||||
.replace(/(invite\.icon)/g, inviteObject?.icon ?? '')
|
||||
// Resolver todas las partes (las literales quedan tal cual)
|
||||
const resolved = await Promise.all(parts.map(p => Promise.resolve(p as any)));
|
||||
return resolved.join('');
|
||||
}
|
||||
Reference in New Issue
Block a user