Spaces:
Paused
Paused
File size: 7,421 Bytes
ee826ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | /**
* ============================================================
* identity-resolver.js β ResoluciΓ³n de Miembros en 5 Capas
* ============================================================
* 1. Discord ID (snowflake) β confianza 1.0
* 2. MenciΓ³n Discord (<@ID>) β confianza 1.0
* 3. Username exacto en DB β confianza 0.95
* 4. Coincidencia exacta en Discord β confianza 0.9
* 5. Fuzzy matching (Levenshtein) β confianza variable
*/
import * as db from './db.js';
// ββ Levenshtein distance ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function levenshtein(a, b) {
const dp = Array.from({length: a.length+1}, (_,i) =>
Array.from({length: b.length+1}, (_,j) => i===0?j:j===0?i:0));
for (let i=1;i<=a.length;i++)
for (let j=1;j<=b.length;j++)
dp[i][j] = a[i-1]===b[j-1] ? dp[i-1][j-1] : 1+Math.min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]);
return dp[a.length][b.length];
}
function similarityScore(a, b) {
if (!a||!b) return 0;
const dist = levenshtein(a.toLowerCase(), b.toLowerCase());
return 1 - dist / Math.max(a.length, b.length);
}
// ββ Identity Cache ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export const identityCache = new Map(); // discordId β fullProfile
let _guild = null;
export async function buildIdentityCache(guild) {
if (!guild) return;
_guild = guild;
try {
const members = await guild.members.fetch();
for (const [id, member] of members) {
const dbRecord = await db.getUserWithRoles(id).catch(() => null);
identityCache.set(id, {
discordId : id,
discordUsername : member.user.username,
displayName : member.displayName,
roles : member.roles.cache.map(r => r.name).filter(n => n !== '@everyone'),
minecraftUsername: dbRecord?.minecraft_username || null,
dbRecord : dbRecord || null,
cachedAt : Date.now(),
});
}
console.log('[IDENTITY] Cache construido: ' + identityCache.size + ' miembros');
} catch (e) {
console.error('[IDENTITY] Error construyendo cache:', e.message);
}
}
export function invalidateIdentityCache(userId) {
identityCache.delete(userId);
console.log('[IDENTITY] Cache invalidado para ' + userId);
}
// ββ Resolver principal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export async function resolvePlayer(identifier, guildMembers, dbRef = null) {
const dbInst = dbRef ?? db;
if (!identifier) return { member: null, dbRecord: null, confidence: 0, method: 'empty' };
// CAPA 1: Discord snowflake ID
if (/^\d{17,19}$/.test(identifier)) {
const member = guildMembers?.get?.(identifier) ?? _guild?.members.cache.get(identifier);
if (member) {
const dbRecord = await dbInst.getUserWithRoles(identifier).catch(() => null);
return { member, dbRecord, confidence: 1.0, method: 'discord_id' };
}
}
// CAPA 2: MenciΓ³n <@ID> o <@!ID>
const mentionMatch = identifier.match(/^<@!?(\d{17,19})>$/);
if (mentionMatch) return resolvePlayer(mentionMatch[1], guildMembers, dbRef);
const q = identifier.toLowerCase();
// CAPA 3: Username exacto en DB
try {
const rows = await dbInst.db.execute({
sql : 'SELECT * FROM users WHERE LOWER(username)=? OR LOWER(nickname)=? LIMIT 1',
args: [q, q],
});
if (rows.rows.length > 0) {
const rec = rows.rows[0];
const member = guildMembers?.find?.(m => m.id === rec.user_id) || _guild?.members.cache.get(rec.user_id);
return { member: member || null, dbRecord: rec, confidence: 0.95, method: 'exact_db' };
}
} catch {}
// CAPA 4: Coincidencia exacta en Discord
const members = guildMembers ?? _guild?.members.cache;
if (members) {
const exactMember = members.find?.(m =>
m.user.username.toLowerCase() === q ||
(m.nickname ?? '').toLowerCase() === q ||
m.displayName.toLowerCase() === q
);
if (exactMember) {
const dbRecord = await dbInst.getUserWithRoles(exactMember.id).catch(() => null);
return { member: exactMember, dbRecord, confidence: 0.9, method: 'exact_discord' };
}
}
// CAPA 5: Fuzzy matching
const candidates = [];
if (members) {
for (const [, m] of (members.entries?.() ?? [])) {
const s = Math.max(
similarityScore(identifier, m.user.username),
similarityScore(identifier, m.displayName),
m.nickname ? similarityScore(identifier, m.nickname) : 0
);
if (s > 0.6) candidates.push({ member: m, dbRecord: null, score: s, method: 'fuzzy_discord' });
}
}
// Fuzzy en DB
try {
const all = await dbInst.db.execute({
sql : 'SELECT * FROM users WHERE is_active=1 AND bot=0 LIMIT 200',
args: [],
});
for (const rec of all.rows) {
const s = Math.max(
rec.username ? similarityScore(identifier, rec.username) : 0,
rec.nickname ? similarityScore(identifier, rec.nickname) : 0
);
if (s > 0.6) {
const member = _guild?.members.cache.get(rec.user_id) || null;
candidates.push({ member, dbRecord: rec, score: s, method: 'fuzzy_db' });
}
}
} catch {}
if (!candidates.length) return { member: null, dbRecord: null, confidence: 0, method: 'not_found' };
candidates.sort((a, b) => b.score - a.score);
const best = candidates[0];
if (best.member && !best.dbRecord) {
best.dbRecord = await dbInst.getUserWithRoles(best.member.id).catch(() => null);
}
const ambiguous = candidates.filter(c => c.score > best.score - 0.1);
if (ambiguous.length > 1 && best.score < 0.9) {
return {
member : best.member,
dbRecord : best.dbRecord,
confidence : best.score,
method : 'ambiguous',
alternatives: ambiguous.slice(1, 4),
};
}
return { member: best.member, dbRecord: best.dbRecord, confidence: best.score, method: best.method };
}
// ββ Ambiguity handler para Discord ββββββββββββββββββββββββββββββββββββββββββββ
export async function resolveOrAskDiscord(identifier, guildMembers, replyFn) {
const resolved = await resolvePlayer(identifier, guildMembers);
if (resolved.confidence >= 0.9) return resolved;
if (resolved.confidence >= 0.7) {
await replyFn('β οΈ Asumiendo que te refieres a **' + (resolved.member?.displayName ?? '?') + '** (' + (resolved.confidence*100).toFixed(0) + '% seguro). Si no, usa @menciΓ³n directa.');
return resolved;
}
if (resolved.method === 'ambiguous' || resolved.confidence < 0.7) {
const opts = [resolved, ...(resolved.alternatives ?? [])].slice(0, 3)
.map((c, i) => (i+1) + '. **' + (c.member?.displayName ?? '?') + '** β ' + (c.score*100).toFixed(0) + '% similitud');
await replyFn('β No estoy segura a quiΓ©n te refieres con "' + identifier + '".\n' + opts.join('\n') + '\n\n_Usa @usuario para evitar confusiones._');
return null;
}
await replyFn('β No encontrΓ© a nadie que coincida con "' + identifier + '".');
return null;
}
|