refactor: extrae handleMessageUpsert a webhook-handler.ts y úsalo en WebhookServer

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
main
brobert 1 month ago
parent 7189756182
commit e430fc1d4a

@ -0,0 +1,324 @@
import type { Database } from 'bun:sqlite';
import { ensureUserExists } from '../db';
import { normalizeWhatsAppId, isGroupId } from '../utils/whatsapp';
import { IdentityService } from '../services/identity';
import { AllowedGroups } from '../services/allowed-groups';
import { GroupSyncService } from '../services/group-sync';
import { ResponseQueue } from '../services/response-queue';
import { AdminService } from '../services/admin';
import { CommandService } from '../services/command';
import { TaskService } from '../tasks/service';
import { RateLimiter } from '../services/rate-limit';
import { Metrics } from '../services/metrics';
function getMessageText(message: any): string {
if (!message || typeof message !== 'object') return '';
const text =
message.conversation ||
message?.extendedTextMessage?.text ||
message?.imageMessage?.caption ||
message?.videoMessage?.caption ||
'';
return typeof text === 'string' ? text.trim() : '';
}
export async function handleMessageUpsert(data: any, db: Database): Promise<void> {
if (!data?.key?.remoteJid || !data.message) {
if (process.env.NODE_ENV !== 'test') {
console.log('⚠️ Invalid message format - missing required fields');
console.log(data);
}
return;
}
const messageText = getMessageText(data.message);
if (!messageText) {
if (process.env.NODE_ENV !== 'test') {
console.log('⚠️ Empty or unsupported message content');
}
return;
}
// Determine sender depending on context (group vs DM) and ignore non-user messages
const remoteJid = data.key.remoteJid;
const participant = data.key.participant;
const fromMe = !!data.key.fromMe;
// Ignore broadcasts/status
if (remoteJid === 'status@broadcast' || (typeof remoteJid === 'string' && remoteJid.endsWith('@broadcast'))) {
if (process.env.NODE_ENV !== 'test') {
console.log(' Ignoring broadcast/status message');
}
return;
}
// Ignore our own messages
if (fromMe) {
if (process.env.NODE_ENV !== 'test') {
console.log(' Ignoring message sent by the bot (fromMe=true)');
}
return;
}
// Compute sender JID based on chat type (prefer participantAlt when available due to Baileys change)
const senderRaw = isGroupId(remoteJid)
? (data.key.participantAlt || participant)
: remoteJid;
// Aprender mapping alias→número cuando vienen ambos y difieren (participant vs participantAlt)
if (isGroupId(remoteJid)) {
const pAlt = typeof data.key.participantAlt === 'string' ? data.key.participantAlt : null;
const p = typeof participant === 'string' ? participant : null;
if (pAlt && p) {
try {
const nAlt = normalizeWhatsAppId(pAlt);
const n = normalizeWhatsAppId(p);
if (process.env.NODE_ENV !== 'test') {
console.log('[A0] message.key participants', {
participant: p,
participantAlt: pAlt,
normalized_participant: n,
normalized_participantAlt: nAlt,
alias_upsert: !!(nAlt && n && nAlt !== n)
});
}
if (nAlt && n && nAlt !== n) {
IdentityService.upsertAlias(p, pAlt, 'message.key');
}
} catch {}
}
}
// Normalize sender ID for consistency and validation
const normalizedSenderId = normalizeWhatsAppId(senderRaw);
if (!normalizedSenderId) {
if (process.env.NODE_ENV !== 'test') {
console.debug('⚠️ Invalid sender ID, ignoring message', { remoteJid, participant, fromMe });
}
return;
}
// Avoid processing messages from the bot number
if (process.env.CHATBOT_PHONE_NUMBER && normalizedSenderId === process.env.CHATBOT_PHONE_NUMBER) {
if (process.env.NODE_ENV !== 'test') {
console.log(' Ignoring message from the bot number');
}
return;
}
// Ensure user exists in database (swallow DB errors to keep webhook 200)
let userId: string | null = null;
try {
userId = ensureUserExists(senderRaw, db);
} catch (e) {
if (process.env.NODE_ENV !== 'test') {
console.error('⚠️ Error ensuring user exists, ignoring message:', e);
}
return;
}
if (!userId) {
if (process.env.NODE_ENV !== 'test') {
console.log('⚠️ Failed to ensure user exists, ignoring message');
}
return;
}
const messageTextTrimmed = messageText.trim();
const isAdminCmd = messageTextTrimmed.startsWith('/admin');
// A4: Primer DM "activar" — alta/confirmación idempotente (solo en DM)
if (!isGroupId(remoteJid) && messageTextTrimmed === 'activar') {
const base = (process.env.WEB_BASE_URL || '').trim();
const msg = base
? "Listo, ya puedes reclamar/ser responsable y acceder a la web. Para acceder a la web, envía '/t web' y abre el enlace."
: "Listo, ya puedes reclamar/ser responsable.";
try {
await ResponseQueue.add([{ recipient: normalizedSenderId, message: msg }]);
} catch {}
return;
}
// Etapa 2: Descubrimiento seguro de grupos (modo 'discover')
if (isGroupId(remoteJid)) {
try { AllowedGroups.dbInstance = db; } catch {}
const gatingMode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (gatingMode === 'discover') {
try {
const exists = db
.prepare(`SELECT 1 FROM allowed_groups WHERE group_id = ? LIMIT 1`)
.get(remoteJid);
if (!exists) {
try { await GroupSyncService.ensureGroupLabelAndName(remoteJid); } catch {}
try { AllowedGroups.dbInstance = db; } catch {}
try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {}
try { Metrics.inc('unknown_groups_discovered_total'); } catch {}
try {
const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true';
if (notify && !isAdminCmd) {
const admins = AdminService.getAdmins();
if (admins.length > 0) {
const info = remoteJid;
const msg = `🔎 Nuevo grupo detectado: ${info}\nEstado: pending.\nUsa /admin habilitar-aquí desde el grupo o /admin allow-group ${info}.`;
await ResponseQueue.add(admins.map(a => ({ recipient: a, message: msg })));
}
}
} catch {}
if (!isAdminCmd) return;
}
} catch {
// Si la tabla no existe por alguna razón, intentar upsert y retornar igualmente
try { await GroupSyncService.ensureGroupLabelAndName(remoteJid); } catch {}
try { AllowedGroups.dbInstance = db; } catch {}
try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {}
try { Metrics.inc('unknown_groups_discovered_total'); } catch {}
try {
const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true';
if (notify && !isAdminCmd) {
const admins = AdminService.getAdmins();
if (admins.length > 0) {
const info = remoteJid;
const msg = `🔎 Nuevo grupo detectado: ${info}\nEstado: pending.\nUsa /admin habilitar-aquí desde el grupo o /admin allow-group ${info}.`;
await ResponseQueue.add(admins.map(a => ({ recipient: a, message: msg })));
}
}
} catch {}
if (!isAdminCmd) return;
}
}
}
// Etapa 3: Gating en modo 'enforce' — ignorar mensajes de grupos no permitidos
if (isGroupId(remoteJid)) {
try { AllowedGroups.dbInstance = db; } catch {}
const gatingMode2 = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (gatingMode2 === 'enforce') {
try {
const allowed = AllowedGroups.isAllowed(remoteJid);
if (!allowed && !isAdminCmd) {
try { Metrics.inc('messages_blocked_group_total'); } catch {}
return;
}
} catch {
// Si falla el check por cualquier motivo, ser conservadores y permitir
}
}
}
// Manejo de comandos de administración (/admin) antes de cualquier otra lógica de grupo
if (messageTextTrimmed.startsWith('/admin')) {
try { AdminService.dbInstance = db; } catch {}
try { AllowedGroups.dbInstance = db; } catch {}
const adminResponses = await AdminService.handle({
sender: normalizedSenderId,
groupId: remoteJid,
message: messageText
});
if (adminResponses.length > 0) {
await ResponseQueue.add(adminResponses);
}
return;
}
// Check/ensure group exists (allow DMs always)
if (isGroupId(data.key.remoteJid) && !GroupSyncService.isGroupActive(data.key.remoteJid)) {
// En tests, mantener comportamiento anterior: ignorar mensajes de grupos inactivos
if (process.env.NODE_ENV === 'test') {
return;
}
if (process.env.NODE_ENV !== 'test') {
console.log(' Group not active in cache — ensuring group (no immediate members sync)');
}
try {
GroupSyncService.ensureGroupExists(data.key.remoteJid);
try { GroupSyncService.upsertMemberSeen(data.key.remoteJid, normalizedSenderId); } catch {}
} catch (e) {
if (process.env.NODE_ENV !== 'test') {
console.error('⚠️ Failed to ensure group on-the-fly:', e);
}
}
}
// Forward to command service only if it's a text-ish message and starts with /t or /tarea
if (messageTextTrimmed.startsWith('/tarea') || messageTextTrimmed.startsWith('/t')) {
// Rate limiting básico por usuario (desactivado en tests)
if (process.env.NODE_ENV !== 'test') {
const allowed = RateLimiter.checkAndConsume(normalizedSenderId);
if (!allowed) {
// Notificar como máximo una vez por minuto
if (RateLimiter.shouldNotify(normalizedSenderId)) {
await ResponseQueue.add([{
recipient: normalizedSenderId,
message: `Has superado el límite de ${((() => { const v = Number(process.env.RATE_LIMIT_PER_MIN); return Number.isFinite(v) && v > 0 ? v : 15; })())} comandos por minuto. Inténtalo de nuevo en un momento.`
}]);
}
return;
}
}
// Extraer menciones desde el mensaje (varios formatos)
const mentions = data.message?.contextInfo?.mentionedJid
|| data.message?.extendedTextMessage?.contextInfo?.mentionedJid
|| data.message?.imageMessage?.contextInfo?.mentionedJid
|| data.message?.videoMessage?.contextInfo?.mentionedJid
|| [];
// Asegurar que CommandService y TaskService usen la misma DB (tests/producción)
CommandService.dbInstance = db;
TaskService.dbInstance = db;
// Delegar el manejo del comando
const messageId = typeof data?.key?.id === 'string' ? data.key.id : null;
const participantForKey = typeof data?.key?.participantAlt === 'string'
? data.key.participantAlt
: (typeof data?.key?.participant === 'string' ? data.key.participant : null);
const outcome = await CommandService.handleWithOutcome({
sender: normalizedSenderId,
groupId: data.key.remoteJid,
message: messageText,
mentions,
messageId: messageId || undefined,
participant: participantForKey || undefined,
fromMe: !!data?.key?.fromMe
});
const responses = outcome.responses;
// Encolar respuestas si las hay
if (responses.length > 0) {
await ResponseQueue.add(responses);
}
// Reaccionar al mensaje del comando con outcome explícito
try {
const reactionsEnabled = String(process.env.REACTIONS_ENABLED || 'false').toLowerCase();
const enabled = ['true','1','yes','on'].includes(reactionsEnabled);
if (!enabled) return;
if (!messageId) return;
const scope = String(process.env.REACTIONS_SCOPE || 'groups').toLowerCase();
const isGroup = isGroupId(data.key.remoteJid);
if (scope !== 'all' && !isGroup) return;
// Respetar gating 'enforce'
try { AllowedGroups.dbInstance = db; } catch {}
const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (mode === 'enforce' && isGroup) {
try {
if (!AllowedGroups.isAllowed(data.key.remoteJid)) {
return;
}
} catch {}
}
const emoji = outcome.ok ? '🤖' : '⚠️';
const participant = typeof data?.key?.participantAlt === 'string'
? data.key.participantAlt
: (typeof data?.key?.participant === 'string' ? data.key.participant : undefined);
await ResponseQueue.enqueueReaction(data.key.remoteJid, messageId, emoji, { participant, fromMe: !!data?.key?.fromMe });
} catch (e) {
// No romper el flujo por errores de reacción
if (process.env.NODE_ENV !== 'test') {
console.warn('⚠️ Reaction enqueue failed:', e);
}
}
}
}

@ -5,17 +5,11 @@ import { ContactsService } from './services/contacts';
import { Migrator } from './db/migrator'; import { Migrator } from './db/migrator';
import { Metrics } from './services/metrics'; import { Metrics } from './services/metrics';
import { AllowedGroups } from './services/allowed-groups'; import { AllowedGroups } from './services/allowed-groups';
import { db, ensureUserExists } from './db'; import { db } from './db';
import { handleMetricsRequest } from './http/metrics'; import { handleMetricsRequest } from './http/metrics';
import { handleHealthRequest } from './http/health'; import { handleHealthRequest } from './http/health';
import { startServices } from './http/bootstrap'; import { startServices } from './http/bootstrap';
import { normalizeWhatsAppId, isGroupId } from './utils/whatsapp'; import { handleMessageUpsert as handleMessageUpsertFn } from './http/webhook-handler';
import { IdentityService } from './services/identity';
import { ResponseQueue } from './services/response-queue';
import { AdminService } from './services/admin';
import { CommandService } from './services/command';
import { TaskService } from './tasks/service';
import { RateLimiter } from './services/rate-limit';
// Bun is available globally when running under Bun runtime // Bun is available globally when running under Bun runtime
declare global { declare global {
@ -163,305 +157,7 @@ export class WebhookServer {
} }
static async handleMessageUpsert(data: any) { static async handleMessageUpsert(data: any) {
if (!data?.key?.remoteJid || !data.message) { return await handleMessageUpsertFn(data, WebhookServer.dbInstance);
if (process.env.NODE_ENV !== 'test') {
console.log('⚠️ Invalid message format - missing required fields');
console.log(data);
}
return;
}
const messageText = WebhookServer.getMessageText(data.message);
if (!messageText) {
if (process.env.NODE_ENV !== 'test') {
console.log('⚠️ Empty or unsupported message content');
}
return;
}
// Determine sender depending on context (group vs DM) and ignore non-user messages
const remoteJid = data.key.remoteJid;
const participant = data.key.participant;
const fromMe = !!data.key.fromMe;
// Ignore broadcasts/status
if (remoteJid === 'status@broadcast' || (typeof remoteJid === 'string' && remoteJid.endsWith('@broadcast'))) {
if (process.env.NODE_ENV !== 'test') {
console.log(' Ignoring broadcast/status message');
}
return;
}
// Ignore our own messages
if (fromMe) {
if (process.env.NODE_ENV !== 'test') {
console.log(' Ignoring message sent by the bot (fromMe=true)');
}
return;
}
// Compute sender JID based on chat type (prefer participantAlt when available due to Baileys change)
const senderRaw = isGroupId(remoteJid)
? (data.key.participantAlt || participant)
: remoteJid;
// Aprender mapping alias→número cuando vienen ambos y difieren (participant vs participantAlt)
if (isGroupId(remoteJid)) {
const pAlt = typeof data.key.participantAlt === 'string' ? data.key.participantAlt : null;
const p = typeof participant === 'string' ? participant : null;
if (pAlt && p) {
try {
const nAlt = normalizeWhatsAppId(pAlt);
const n = normalizeWhatsAppId(p);
if (process.env.NODE_ENV !== 'test') {
console.log('[A0] message.key participants', {
participant: p,
participantAlt: pAlt,
normalized_participant: n,
normalized_participantAlt: nAlt,
alias_upsert: !!(nAlt && n && nAlt !== n)
});
}
if (nAlt && n && nAlt !== n) {
IdentityService.upsertAlias(p, pAlt, 'message.key');
}
} catch {}
}
}
// Normalize sender ID for consistency and validation
const normalizedSenderId = normalizeWhatsAppId(senderRaw);
if (!normalizedSenderId) {
if (process.env.NODE_ENV !== 'test') {
console.debug('⚠️ Invalid sender ID, ignoring message', { remoteJid, participant, fromMe });
}
return;
}
// Avoid processing messages from the bot number
if (process.env.CHATBOT_PHONE_NUMBER && normalizedSenderId === process.env.CHATBOT_PHONE_NUMBER) {
if (process.env.NODE_ENV !== 'test') {
console.log(' Ignoring message from the bot number');
}
return;
}
// Ensure user exists in database (swallow DB errors to keep webhook 200)
let userId: string | null = null;
try {
userId = ensureUserExists(senderRaw, WebhookServer.dbInstance);
} catch (e) {
if (process.env.NODE_ENV !== 'test') {
console.error('⚠️ Error ensuring user exists, ignoring message:', e);
}
return;
}
if (!userId) {
if (process.env.NODE_ENV !== 'test') {
console.log('⚠️ Failed to ensure user exists, ignoring message');
}
return;
}
const messageTextTrimmed = messageText.trim();
const isAdminCmd = messageTextTrimmed.startsWith('/admin');
// A4: Primer DM "activar" — alta/confirmación idempotente (solo en DM)
if (!isGroupId(remoteJid) && messageTextTrimmed === 'activar') {
const base = (process.env.WEB_BASE_URL || '').trim();
const msg = base
? "Listo, ya puedes reclamar/ser responsable y acceder a la web. Para acceder a la web, envía '/t web' y abre el enlace."
: "Listo, ya puedes reclamar/ser responsable.";
try {
await ResponseQueue.add([{ recipient: normalizedSenderId, message: msg }]);
} catch {}
return;
}
// Etapa 2: Descubrimiento seguro de grupos (modo 'discover')
if (isGroupId(remoteJid)) {
try { AllowedGroups.dbInstance = WebhookServer.dbInstance; } catch {}
const gatingMode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (gatingMode === 'discover') {
try {
const exists = WebhookServer.dbInstance
.prepare(`SELECT 1 FROM allowed_groups WHERE group_id = ? LIMIT 1`)
.get(remoteJid);
if (!exists) {
try { await GroupSyncService.ensureGroupLabelAndName(remoteJid); } catch {}
try { AllowedGroups.dbInstance = WebhookServer.dbInstance; } catch {}
try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {}
try { Metrics.inc('unknown_groups_discovered_total'); } catch {}
try {
const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true';
if (notify && !isAdminCmd) {
const admins = AdminService.getAdmins();
if (admins.length > 0) {
const info = remoteJid;
const msg = `🔎 Nuevo grupo detectado: ${info}\nEstado: pending.\nUsa /admin habilitar-aquí desde el grupo o /admin allow-group ${info}.`;
await ResponseQueue.add(admins.map(a => ({ recipient: a, message: msg })));
}
}
} catch {}
if (!isAdminCmd) return;
}
} catch {
// Si la tabla no existe por alguna razón, intentar upsert y retornar igualmente
try { await GroupSyncService.ensureGroupLabelAndName(remoteJid); } catch {}
try { AllowedGroups.dbInstance = WebhookServer.dbInstance; } catch {}
try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {}
try { Metrics.inc('unknown_groups_discovered_total'); } catch {}
try {
const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true';
if (notify && !isAdminCmd) {
const admins = AdminService.getAdmins();
if (admins.length > 0) {
const info = remoteJid;
const msg = `🔎 Nuevo grupo detectado: ${info}\nEstado: pending.\nUsa /admin habilitar-aquí desde el grupo o /admin allow-group ${info}.`;
await ResponseQueue.add(admins.map(a => ({ recipient: a, message: msg })));
}
}
} catch {}
if (!isAdminCmd) return;
}
}
}
// Etapa 3: Gating en modo 'enforce' — ignorar mensajes de grupos no permitidos
if (isGroupId(remoteJid)) {
try { AllowedGroups.dbInstance = WebhookServer.dbInstance; } catch {}
const gatingMode2 = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (gatingMode2 === 'enforce') {
try {
const allowed = AllowedGroups.isAllowed(remoteJid);
if (!allowed && !isAdminCmd) {
try { Metrics.inc('messages_blocked_group_total'); } catch {}
return;
}
} catch {
// Si falla el check por cualquier motivo, ser conservadores y permitir
}
}
}
// Manejo de comandos de administración (/admin) antes de cualquier otra lógica de grupo
if (messageTextTrimmed.startsWith('/admin')) {
try { AdminService.dbInstance = WebhookServer.dbInstance; } catch {}
try { AllowedGroups.dbInstance = WebhookServer.dbInstance; } catch {}
const adminResponses = await AdminService.handle({
sender: normalizedSenderId,
groupId: remoteJid,
message: messageText
});
if (adminResponses.length > 0) {
await ResponseQueue.add(adminResponses);
}
return;
}
// Check/ensure group exists (allow DMs always)
if (isGroupId(data.key.remoteJid) && !GroupSyncService.isGroupActive(data.key.remoteJid)) {
// En tests, mantener comportamiento anterior: ignorar mensajes de grupos inactivos
if (process.env.NODE_ENV === 'test') {
return;
}
if (process.env.NODE_ENV !== 'test') {
console.log(' Group not active in cache — ensuring group (no immediate members sync)');
}
try {
GroupSyncService.ensureGroupExists(data.key.remoteJid);
try { GroupSyncService.upsertMemberSeen(data.key.remoteJid, normalizedSenderId); } catch {}
} catch (e) {
if (process.env.NODE_ENV !== 'test') {
console.error('⚠️ Failed to ensure group on-the-fly:', e);
}
}
}
// Forward to command service only if it's a text-ish message and starts with /t or /tarea
// messageTextTrimmed computed earlier
if (messageTextTrimmed.startsWith('/tarea') || messageTextTrimmed.startsWith('/t')) {
// Rate limiting básico por usuario (desactivado en tests)
if (process.env.NODE_ENV !== 'test') {
const allowed = RateLimiter.checkAndConsume(normalizedSenderId);
if (!allowed) {
// Notificar como máximo una vez por minuto
if (RateLimiter.shouldNotify(normalizedSenderId)) {
await ResponseQueue.add([{
recipient: normalizedSenderId,
message: `Has superado el límite de ${((() => { const v = Number(process.env.RATE_LIMIT_PER_MIN); return Number.isFinite(v) && v > 0 ? v : 15; })())} comandos por minuto. Inténtalo de nuevo en un momento.`
}]);
}
return;
}
}
// Extraer menciones desde el mensaje (varios formatos)
const mentions = data.message?.contextInfo?.mentionedJid
|| data.message?.extendedTextMessage?.contextInfo?.mentionedJid
|| data.message?.imageMessage?.contextInfo?.mentionedJid
|| data.message?.videoMessage?.contextInfo?.mentionedJid
|| [];
// Asegurar que CommandService y TaskService usen la misma DB (tests/producción)
CommandService.dbInstance = WebhookServer.dbInstance;
TaskService.dbInstance = WebhookServer.dbInstance;
// Delegar el manejo del comando
const messageId = typeof data?.key?.id === 'string' ? data.key.id : null;
const participantForKey = typeof data?.key?.participantAlt === 'string'
? data.key.participantAlt
: (typeof data?.key?.participant === 'string' ? data.key.participant : null);
const outcome = await CommandService.handleWithOutcome({
sender: normalizedSenderId,
groupId: data.key.remoteJid,
message: messageText,
mentions,
messageId: messageId || undefined,
participant: participantForKey || undefined,
fromMe: !!data?.key?.fromMe
});
const responses = outcome.responses;
// Encolar respuestas si las hay
if (responses.length > 0) {
await ResponseQueue.add(responses);
}
// Reaccionar al mensaje del comando con outcome explícito
try {
const reactionsEnabled = String(process.env.REACTIONS_ENABLED || 'false').toLowerCase();
const enabled = ['true','1','yes','on'].includes(reactionsEnabled);
if (!enabled) return;
if (!messageId) return;
const scope = String(process.env.REACTIONS_SCOPE || 'groups').toLowerCase();
const isGroup = isGroupId(data.key.remoteJid);
if (scope !== 'all' && !isGroup) return;
// Respetar gating 'enforce'
try { AllowedGroups.dbInstance = WebhookServer.dbInstance; } catch {}
const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (mode === 'enforce' && isGroup) {
try {
if (!AllowedGroups.isAllowed(data.key.remoteJid)) {
return;
}
} catch {}
}
const emoji = outcome.ok ? '🤖' : '⚠️';
const participant = typeof data?.key?.participantAlt === 'string'
? data.key.participantAlt
: (typeof data?.key?.participant === 'string' ? data.key.participant : undefined);
await ResponseQueue.enqueueReaction(data.key.remoteJid, messageId, emoji, { participant, fromMe: !!data?.key?.fromMe });
} catch (e) {
// No romper el flujo por errores de reacción
if (process.env.NODE_ENV !== 'test') {
console.warn('⚠️ Reaction enqueue failed:', e);
}
}
}
} }
static validateEnv() { static validateEnv() {

Loading…
Cancel
Save