From 2e49edfd04aa7a65448227982751a722feb2b5dc Mon Sep 17 00:00:00 2001 From: Arpit Dilip Tagade <143588157+tagadearpit@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:51:29 +0000 Subject: [PATCH] fix: resolve LID identities in sender mentions --- handler.js | 133 ++++++++++++-------- plugins/greet.js | 290 +++++++++++++++++++++++++++++++------------- plugins/leveling.js | 76 +++++++++--- 3 files changed, 347 insertions(+), 152 deletions(-) diff --git a/handler.js b/handler.js index 538aa6c5..fbd6f568 100644 --- a/handler.js +++ b/handler.js @@ -20,6 +20,56 @@ function jidToNum(jid) { if (!jid) return ''; return jidNormalizedUser(jid).split('@')[0].replace(/\D/g, ''); } +function normalizePhoneJid(value) { + if (!value) return ''; + + const normalized = jidNormalizedUser(value); + + if (normalized.endsWith('@s.whatsapp.net')) { + return normalized; + } + + const number = normalized.split('@')[0].replace(/\D/g, ''); + return number ? `${number}@s.whatsapp.net` : normalized; +} + +function resolveLid(jid, groupMetadata = null) { + if (!jid || typeof jid !== 'string') return jid || ''; + if (!jid.endsWith('@lid')) return jidNormalizedUser(jid); + + const participants = groupMetadata?.participants || []; + + for (const participant of participants) { + const participantLid = participant.lid || ''; + + if ( + participantLid && + ( + participantLid === jid || + jidNormalizedUser(participantLid) === jidNormalizedUser(jid) + ) + ) { + return normalizePhoneJid(participant.id); + } + } + + const normalizedLid = jidNormalizedUser(jid); + const bareLid = normalizedLid.split('@')[0]; + + const cachedPhone = + global.lidPhoneCache?.get(jid) || + global.lidPhoneCache?.get(normalizedLid) || + global.lidPhoneCache?.get(bareLid) || + global.lidPhoneCache?.get(`${bareLid}@lid`); + + if (cachedPhone) { + return normalizePhoneJid(cachedPhone); + } + + // Resolution is best-effort. Preserve the original valid JID if no + // phone mapping exists. + return normalizedLid; +} // True when two phone numbers refer to the same subscriber. // Handles missing country codes by comparing the last 9 significant digits. @@ -271,16 +321,7 @@ async function handleMessages(sock, message) { : rawMsg) || rawMsg; - // jid = the chat to respond to — always m.key.remoteJid, NEVER reconstructed. - // Valid suffixes: @s.whatsapp.net (regular WA), @lid (Business / privacy DM), - // @g.us (group). Do NOT remap @lid to @s.whatsapp.net here — safeSend handles - // session establishment and phone-JID fallback automatically. const jid = message.key.remoteJid; - // from = the individual who typed the command. - // For fromMe private messages participant is undefined and jid is the - // recipient — not the sender. Correct this so ctx.from always refers - // to the actual sender (bot's own JID when fromMe, participant when in - // a group, or the remote JID for incoming private messages). const _botOwnJid = global.botJid || ''; const from = message.key.participant || (message.key.fromMe && !isJidGroup(jid) ? (_botOwnJid || jid) : jid); @@ -374,11 +415,14 @@ async function handleMessages(sock, message) { if (URL_REGEX.test(text)) { try { await sock.sendMessage(jid, { delete: message.key }); - const antlinkMsg = getStr('antlink') || `⚠️ @${from.split('@')[0]} links are not allowed in this group.`; - await safeSend(sock, jid, { - text: antlinkMsg, - mentions: [from] - }); + const antlinkMsg = + getStr('antlink') || + `⚠️ @${senderNum} links are not allowed in this group.`; + + await safeSend(sock, jid, { + text: antlinkMsg, + mentions: [resolvedFrom], + }); } catch (e) { console.error('[Antilink] delete failed:', e.message); } @@ -390,9 +434,9 @@ async function handleMessages(sock, message) { // ── onMessage hooks — fired for ALL messages (not just commands) ──────── // Tracking and auto-reply only for other people's messages (not the bot's own). if (!message.key.fromMe) { - if (typeof global.trackMessage === 'function') try { global.trackMessage(jid, from); } catch {} + if (typeof global.trackMessage === 'function') try { global.trackMessage(jid, resolvedFrom); } catch {} if (typeof global.addXP === 'function') { - try { global.addXP(jid, from); } catch {} + try { global.addXP(jid, resolvedFrom); } catch {} } if (!isGroup && typeof global.checkAutoReply === 'function') { try { @@ -406,7 +450,10 @@ async function handleMessages(sock, message) { try { const result = global.checkWelcomeQuizAnswer(jid, from, text); if (result?.passed) { - await safeSend(sock, jid, { text: `✅ @${from.split('@')[0]} passed the welcome quiz! Welcome to the group! 🎉`, mentions: [from] }); + await safeSend(sock, jid, { + text: `✅ @${senderNum} passed the welcome quiz! Welcome to the group! 🎉`, + mentions: [resolvedFrom] + }); } } catch {} } @@ -418,9 +465,16 @@ async function handleMessages(sock, message) { if (typeof p.onMessage !== 'function') continue; try { await p.onMessage(sock, message, text, { - jid, sender, from, isGroup, - contextInfo: isGroup ? {} : GLOBAL_CONTEXT_INFO - }); + jid, + sender, + from, + resolvedFrom, + senderNum, + resolveLid: (targetJid) => resolveLid(targetJid, groupMetadata), + isGroup, + groupMetadata, + contextInfo: isGroup ? {} : GLOBAL_CONTEXT_INFO + }); } catch { /* ignore plugin onMessage errors */ } } @@ -514,39 +568,9 @@ async function handleMessages(sock, message) { if (isGroup) { groupMetadata = await getCachedGroupMetadata(sock, jid); } - - // ── Resolve sender phone (handle @lid format) ───────────────────────── - const isLid = typeof from === 'string' && from.endsWith('@lid'); - let resolvedFrom = from; // will be the real phone JID if LID is resolved - - if (isLid) { - // 1. Try group participants list (most accurate) - if (groupMetadata?.participants) { - for (const p of groupMetadata.participants) { - const pLid = p.lid || ''; - if (pLid && (pLid === from || jidNormalizedUser(pLid) === jidNormalizedUser(from))) { - resolvedFrom = p.id; // swap LID for real phone JID - break; - } - } - } - - // 2. Fall back to the global LID→phone cache populated by silva.js - // (every received message caches participant LID + phone via cacheLidPhone) - if (resolvedFrom === from && global.lidPhoneCache?.size) { - const normLid = from.split(':')[0].split('@')[0]; - const cachedPhone = global.lidPhoneCache.get(normLid) - || global.lidPhoneCache.get(normLid + '@lid') - || global.lidPhoneCache.get(from); - if (cachedPhone) { - resolvedFrom = cachedPhone.includes('@') - ? cachedPhone - : `${cachedPhone.replace(/\D/g, '')}@s.whatsapp.net`; - } - } - } - - const fromNum = jidToNum(resolvedFrom); + const resolvedFrom = resolveLid(from, groupMetadata); + const senderNum = jidToNum(resolvedFrom); + const fromNum = senderNum; // ── Resolve owner / bot phone numbers ───────────────────────────────── // Pull directly from process.env first so stale config objects can't @@ -615,6 +639,9 @@ async function handleMessages(sock, message) { jid, chat: jid, isGroup, + resolvedFrom, + senderNum, + resolveLid: (targetJid) => resolveLid(targetJid, groupMetadata), isAdmin, isBotAdmin, isOwner, diff --git a/plugins/greet.js b/plugins/greet.js index 5865da08..546bfe18 100644 --- a/plugins/greet.js +++ b/plugins/greet.js @@ -1,82 +1,123 @@ 'use strict'; -const fs = require('fs'); -const path = require('path'); +const fs = require('fs'); +const path = require('path'); const { fmt } = require('../lib/theme'); -const config = require('../config'); +const config = require('../config'); const DATA_PATH = path.join(__dirname, '../data/greet.json'); // ── Persistence ─────────────────────────────────────────────────────────────── function loadData() { try { - if (fs.existsSync(DATA_PATH)) return JSON.parse(fs.readFileSync(DATA_PATH, 'utf8')); - } catch { /* ignore */ } + if (fs.existsSync(DATA_PATH)) { + return JSON.parse(fs.readFileSync(DATA_PATH, 'utf8')); + } + } catch { + // Ignore malformed or unavailable persisted data. + } + return {}; } function saveData(data) { try { const dir = path.dirname(DATA_PATH); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(DATA_PATH, JSON.stringify(data, null, 2)); - } catch { /* ignore */ } + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync( + DATA_PATH, + JSON.stringify(data, null, 2) + ); + } catch { + // Ignore persistence errors to avoid breaking message handling. + } } let greetData = loadData(); -// ── Once-per-day tracker (in-memory, resets on bot restart / midnight) ──────── -// Key: senderJid → Value: 'YYYY-MM-DD' string of the last date greeted. +// ── Once-per-day tracker ────────────────────────────────────────────────────── +// Key: canonical sender JID +// Value: YYYY-MM-DD date of the most recent greeting. +// +// The map is intentionally in memory and resets when the process restarts. const greetedToday = new Map(); function todayStr() { - return new Date().toISOString().slice(0, 10); // e.g. '2026-03-12' + return new Date().toISOString().slice(0, 10); } -function hasGreetedToday(jid) { - return greetedToday.get(jid) === todayStr(); +function hasGreetedToday(senderJid) { + if (!senderJid) return false; + + return greetedToday.get(senderJid) === todayStr(); } -function markGreeted(jid) { - greetedToday.set(jid, todayStr()); +function markGreeted(senderJid) { + if (!senderJid) return; + + greetedToday.set(senderJid, todayStr()); } // ── Helpers ─────────────────────────────────────────────────────────────────── function isEnabled() { - // Default ON — becomes OFF only if explicitly set false - return greetData['__enabled__'] !== false; + // Enabled by default unless explicitly disabled. + return greetData.__enabled__ !== false; } function getGreetText() { - const ownerNum = (process.env.OWNER_NUMBER || global.botNum || '').replace(/\D/g, ''); - const ownerJid = ownerNum ? `${ownerNum}@s.whatsapp.net` : null; + const ownerNum = ( + process.env.OWNER_NUMBER || + global.botNum || + '' + ).replace(/\D/g, ''); + + const ownerJid = ownerNum + ? `${ownerNum}@s.whatsapp.net` + : null; + return ( - greetData['__text__'] || + greetData.__text__ || (ownerJid && greetData[ownerJid]) || - Object.values(greetData).find(v => typeof v === 'string') || + Object.values(greetData).find( + value => typeof value === 'string' + ) || null ); } // ── Bootstrap from GREETING env var ────────────────────────────────────────── -// Lets the owner pre-set a greeting via GREETING= in secrets/config without -// ever running .setgreet. Skipped if owner has already set one manually. +// Allows the owner to preconfigure a greeting through environment variables. +// Existing manually configured text takes precedence. (function bootstrap() { const envGreet = (config.GREETING || '').trim(); + if (!envGreet) return; - if (!greetData['__text__']) { - greetData['__text__'] = envGreet; - saveData(greetData); - } + if (greetData.__text__) return; + + greetData.__text__ = envGreet; + saveData(greetData); })(); // ───────────────────────────────────────────────────────────────────────────── module.exports = { - commands: ['setgreet', 'getgreet', 'delgreet', 'greeton', 'greetoff'], - description: 'Auto-greeting sent once per day to anyone who DMs the bot', - permission: 'owner', - group: false, - private: true, + commands: [ + 'setgreet', + 'getgreet', + 'delgreet', + 'greeton', + 'greetoff' + ], + + description: + 'Auto-greeting sent once per day to anyone who DMs the bot', + + permission: 'owner', + group: false, + private: true, async run(sock, message, args, ctx) { const { reply } = ctx; @@ -84,96 +125,183 @@ module.exports = { // ── Toggle ON ────────────────────────────────────────────────────── if (cmd === 'greeton') { - greetData['__enabled__'] = true; + greetData.__enabled__ = true; saveData(greetData); + const txt = getGreetText(); - return reply(fmt( - `✅ *Greeting enabled!*\n\n` + - (txt - ? `📝 Current message:\n_"${txt}"_` - : `⚠️ No greeting text set yet.\nUse \`.setgreet \` to set one.`) - )); + + return reply( + fmt( + `✅ *Greeting enabled!*\n\n` + + ( + txt + ? `📝 Current message:\n_"${txt}"_` + : ( + `⚠️ No greeting text set yet.\n` + + `Use \`.setgreet \` to set one.` + ) + ) + ) + ); } // ── Toggle OFF ───────────────────────────────────────────────────── if (cmd === 'greetoff') { - greetData['__enabled__'] = false; + greetData.__enabled__ = false; saveData(greetData); - return reply(fmt('❌ *Greeting disabled.*\n\nPeople who message the bot will not receive an auto-reply.')); + + return reply( + fmt( + `❌ *Greeting disabled.*\n\n` + + `People who message the bot will not receive an auto-reply.` + ) + ); } // ── Set greeting text ────────────────────────────────────────────── if (cmd === 'setgreet') { const text = args.join(' ').trim(); + if (!text) { - return reply(fmt( - `📝 *Usage:* \`.setgreet \`\n\n` + - `_Example:_\n\`.setgreet Hey! I'm busy right now, I'll reply soon 😊\`\n\n` + - `*Tip:* You can also set \`GREETING=\` in your Replit Secrets to pre-load a greeting automatically.` - )); + return reply( + fmt( + `📝 *Usage:* \`.setgreet \`\n\n` + + `_Example:_\n` + + `\`.setgreet Hey! I'm busy right now, I'll reply soon 😊\`\n\n` + + `*Tip:* You can also set \`GREETING=\` in your ` + + `Replit Secrets to pre-load a greeting automatically.` + ) + ); } - greetData['__text__'] = text; - // Greeting is auto-enabled when text is set - if (greetData['__enabled__'] !== false) greetData['__enabled__'] = true; + + greetData.__text__ = text; + + // Setting greeting text automatically enables greeting unless the + // owner explicitly disabled it. + if (greetData.__enabled__ !== false) { + greetData.__enabled__ = true; + } + saveData(greetData); - // Clear today's cache so updated greeting goes out fresh + + // Allow the updated greeting to be sent again immediately. greetedToday.clear(); - return reply(fmt( - `✅ *Greeting set!*\n\n_"${text}"_\n\n` + - `People who DM the bot will receive this *once per day*.\n` + - `Use \`.greetoff\` to pause it anytime.` - )); + + return reply( + fmt( + `✅ *Greeting set!*\n\n` + + `_"${text}"_\n\n` + + `People who DM the bot will receive this *once per day*.\n` + + `Use \`.greetoff\` to pause it anytime.` + ) + ); } // ── View current greeting ────────────────────────────────────────── if (cmd === 'getgreet') { const txt = getGreetText(); - const on = isEnabled(); + const on = isEnabled(); + if (!txt) { - return reply(fmt('❌ No greeting set.\n\nUse `.setgreet ` to set one.')); + return reply( + fmt( + `❌ No greeting set.\n\n` + + `Use \`.setgreet \` to set one.` + ) + ); } - return reply(fmt( - `📝 *Current Greeting*\n\n` + - `_"${txt}"_\n\n` + - `Status: ${on ? '✅ *ON* — sent once per day' : '❌ *OFF* — paused'}\n\n` + - `• \`.greeton\` / \`.greetoff\` — toggle\n` + - `• \`.setgreet \` — change message\n` + - `• \`.delgreet\` — delete` - )); + + return reply( + fmt( + `📝 *Current Greeting*\n\n` + + `_"${txt}"_\n\n` + + `Status: ${ + on + ? '✅ *ON* — sent once per day' + : '❌ *OFF* — paused' + }\n\n` + + `• \`.greeton\` / \`.greetoff\` — toggle\n` + + `• \`.setgreet \` — change message\n` + + `• \`.delgreet\` — delete` + ) + ); } // ── Delete greeting ──────────────────────────────────────────────── if (cmd === 'delgreet') { const txt = getGreetText(); - if (!txt) return reply(fmt('❌ No greeting to delete.')); - delete greetData['__text__']; - // Also remove any legacy owner-JID keyed entries - Object.keys(greetData).forEach(k => { - if (k !== '__enabled__' && k !== '__text__') delete greetData[k]; + + if (!txt) { + return reply( + fmt('❌ No greeting to delete.') + ); + } + + delete greetData.__text__; + + // Remove legacy owner-JID keyed greeting entries. + Object.keys(greetData).forEach(key => { + if ( + key !== '__enabled__' && + key !== '__text__' + ) { + delete greetData[key]; + } }); + saveData(greetData); greetedToday.clear(); - return reply(fmt('✅ Greeting *deleted*.')); + + return reply( + fmt('✅ Greeting *deleted*.') + ); } }, - // ── onMessage hook — fires for every incoming private message ───────────── - onMessage: async (sock, message, text, { jid, isGroup }) => { + // ── onMessage hook ──────────────────────────────────────────────────────── + // Fires for every incoming private message. + onMessage: async ( + sock, + message, + text, + { + jid, + resolvedFrom, + isGroup + } + ) => { if (isGroup) return; if (message.key.fromMe) return; - - // Respect the on/off toggle if (!isEnabled()) return; - // Only greet each person once per calendar day - if (hasGreetedToday(jid)) return; + /* + * `jid` is the destination chat used when sending the response. + * + * `resolvedFrom` is the canonical participant identity exposed by + * handler.js. It may map an incoming @lid identity to the corresponding + * @s.whatsapp.net JID. + * + * Use the canonical identity for once-per-day tracking, but continue + * sending the response to the original chat JID. + */ + const canonicalSender = resolvedFrom || jid; + + if (hasGreetedToday(canonicalSender)) return; const greet = getGreetText(); + if (!greet) return; try { - await sock.sendMessage(jid, { text: greet }, { quoted: message }); - markGreeted(jid); - } catch { /* ignore */ } + await sock.sendMessage( + jid, + { text: greet }, + { quoted: message } + ); + + markGreeted(canonicalSender); + } catch { + // Greeting failure must not interrupt normal message processing. + } } -}; +}; \ No newline at end of file diff --git a/plugins/leveling.js b/plugins/leveling.js index a77df9fa..34f7bf1e 100644 --- a/plugins/leveling.js +++ b/plugins/leveling.js @@ -81,29 +81,59 @@ module.exports = { private: false, run: async (sock, message, args, ctx) => { - const { jid, sender, contextInfo } = ctx; + const { + jid, + sender, + resolvedFrom, + senderNum, + resolveLid, + contextInfo + } = ctx; const rawCmd = (message.message?.extendedTextMessage?.text || message.message?.conversation || '').trim().split(/\s+/)[0].replace(/^\./, '').toLowerCase(); if (['leaderboard', 'lb', 'levels'].includes(rawCmd)) { - const groupUsers = Object.entries(levelData) - .filter(([key]) => key.startsWith(jid + ':')) - .map(([key, data]) => ({ sender: key.split(':')[1], ...data })) - .sort((a, b) => b.level - a.level || b.xp - a.xp) - .slice(0, 15); - - if (!groupUsers.length) { - return sock.sendMessage(jid, { text: '📊 No leveling data yet. Keep chatting!', contextInfo }, { quoted: message }); - } - - const list = groupUsers.map((u, i) => { - const num = u.sender.split('@')[0]; + const groupUsers = Object.entries(levelData) + .filter(([key]) => key.startsWith(`${jid}:`)) + .map(([key, data]) => { + const storedJid = key.slice(jid.length + 1); + + const mentionJid = typeof resolveLid === 'function' + ? resolveLid(storedJid) + : storedJid; + + return { + sender: storedJid, + mentionJid, + ...data + }; + }) + .sort((a, b) => b.level - a.level || b.xp - a.xp) + .slice(0, 15); + + if (!groupUsers.length) { + return sock.sendMessage( + jid, + { + text: '📊 No leveling data yet. Keep chatting!', + contextInfo + }, + { quoted: message } + ); + } + + const list = groupUsers.map((u, i) => { + const num = + u.mentionJid + .split('@')[0] + .replace(/\D/g, '') || + u.mentionJid.split('@')[0]; const title = getTitle(u.level); const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`; return `${medal} @${num}\n Level ${u.level} ${title} • ${u.xp} XP • ${u.messages} msgs`; }).join('\n\n'); - const mentions = groupUsers.map(u => u.sender); + const mentions = groupUsers.map(u => u.mentionJid); return sock.sendMessage(jid, { text: `🏆 *Leaderboard*\n\n${list}`, @@ -112,13 +142,23 @@ module.exports = { }, { quoted: message }); } - const key = `${jid}:${sender}`; - const user = levelData[key] || { xp: 0, level: 0, messages: 0 }; + const userJid = resolvedFrom || sender; + + const key = `${jid}:${userJid}`; + + const user = levelData[key] || { + xp: 0, + level: 0, + messages: 0 + }; const needed = xpForLevel(user.level); const progress = makeProgressBar(user.xp, needed); const title = getTitle(user.level); const nextTitle = titles.find(t => t.level > user.level); - const num = sender.split('@')[0]; + const num = + senderNum || + userJid.split('@')[0].replace(/\D/g, '') || + userJid.split('@')[0] let text = `📊 *Your Level*\n\n`; text += `👤 @${num}\n`; @@ -131,7 +171,7 @@ module.exports = { return sock.sendMessage(jid, { text, - mentions: [sender], + mentions: [userJid], contextInfo }, { quoted: message }); }