Spaces:
Paused
Paused
| /** | |
| * ============================================================ | |
| * 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; | |
| } | |