@omnizap-system/omnizap 2.6.1 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +54 -9
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/security-runner-hardening.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +1 -0
- package/app/config/index.js +2 -0
- package/app/configParts/adminIdentity.js +5 -5
- package/app/configParts/baileysConfig.js +226 -55
- package/app/configParts/groupUtils.js +5 -0
- package/app/configParts/messagePersistenceService.js +143 -3
- package/app/configParts/sessionConfig.js +157 -0
- package/app/connection/baileysCompatibility.test.js +1 -1
- package/app/connection/groupOwnerWriteStateResolver.js +109 -0
- package/app/connection/socketController.js +625 -124
- package/app/connection/socketController.multiSession.test.js +108 -0
- package/app/controllers/messageController.js +1 -1
- package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
- package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
- package/app/controllers/messageProcessingPipeline.js +88 -9
- package/app/controllers/messageProcessingPipeline.test.js +200 -0
- package/app/modules/adminModule/AGENT.md +1 -1
- package/app/modules/adminModule/commandConfig.json +3318 -1347
- package/app/modules/adminModule/groupCommandHandlers.js +856 -14
- package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
- package/app/modules/adminModule/groupWarningRepository.js +152 -0
- package/app/modules/aiModule/AGENT.md +47 -30
- package/app/modules/aiModule/aiConfigRuntime.js +1 -0
- package/app/modules/aiModule/catCommand.js +132 -25
- package/app/modules/aiModule/commandConfig.json +114 -28
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
- package/app/modules/gameModule/AGENT.md +1 -1
- package/app/modules/gameModule/commandConfig.json +29 -0
- package/app/modules/menuModule/AGENT.md +1 -1
- package/app/modules/menuModule/commandConfig.json +45 -10
- package/app/modules/menuModule/menuCatalogService.js +190 -0
- package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
- package/app/modules/menuModule/menuDynamicService.js +511 -0
- package/app/modules/menuModule/menuDynamicService.test.js +141 -0
- package/app/modules/menuModule/menus.js +36 -5
- package/app/modules/playModule/AGENT.md +10 -5
- package/app/modules/playModule/commandConfig.json +74 -16
- package/app/modules/playModule/playCommandConstants.js +13 -7
- package/app/modules/playModule/playCommandCore.js +4 -6
- package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
- package/app/modules/playModule/playConfigRuntime.js +5 -6
- package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/commandConfig.json +145 -0
- package/app/modules/stickerPackModule/AGENT.md +1 -1
- package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
- package/app/modules/stickerPackModule/commandConfig.json +29 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/geminiService.js +131 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/moduleAiHelpCoreService.js +33 -4
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +51 -21
- package/app/services/messaging/newsBroadcastService.js +843 -27
- package/app/services/multiSession/assignmentBalancerService.js +457 -0
- package/app/services/multiSession/groupOwnershipRepository.js +381 -0
- package/app/services/multiSession/groupOwnershipService.js +890 -0
- package/app/services/multiSession/groupOwnershipService.test.js +309 -0
- package/app/services/multiSession/sessionRegistryService.js +293 -0
- package/app/store/aiPromptStore.js +36 -19
- package/app/store/groupConfigStore.js +41 -5
- package/app/store/premiumUserStore.js +21 -7
- package/app/utils/antiLink/antiLinkModule.js +352 -16
- package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
- package/database/index.js +6 -0
- package/database/migrations/20260307_d0_hardening_down.sql +1 -1
- package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
- package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
- package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
- package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
- package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
- package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
- package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
- package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
- package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
- package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
- package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
- package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
- package/database/schema.sql +102 -1
- package/docker-compose.yml +4 -1
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
- package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
- package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
- package/docs/security/omnizap-static-security-headers.conf +25 -0
- package/ecosystem.prod.config.cjs +31 -11
- package/index.js +52 -18
- package/observability/alert-rules.yml +20 -0
- package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
- package/observability/mysql-setup.sql +4 -4
- package/observability/system-admin-observability.md +26 -0
- package/package.json +12 -5
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/stickersApp.js +255 -103
- package/public/js/apps/termsReactApp.js +57 -8
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +96 -47
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/politica-de-privacidade.html +1 -1
- package/public/pages/stickers.html +5 -5
- package/public/pages/termos-de-uso-texto-integral.html +1 -1
- package/public/pages/termos-de-uso.html +1 -1
- package/public/pages/user-password-reset.html +3 -4
- package/public/pages/user-systemadm.html +8 -462
- package/public/pages/user.html +1 -1
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +10 -0
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +155 -0
- package/scripts/new-whatsapp-session.sh +317 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +267 -0
- package/server/controllers/sticker/stickerCatalogController.js +9 -23
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
- package/server/controllers/system/systemController.js +254 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailTemplateService.js +3 -2
- package/server/http/httpServer.js +8 -4
- package/server/middleware/securityHeaders.js +20 -1
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +30 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/static/staticPageRouter.js +27 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +2 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -2,8 +2,8 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { createGeminiTextService } from './geminiService.js';
|
|
4
4
|
|
|
5
|
-
test('createGeminiTextService retorna null quando GEMINI_API_KEY nao existe', () => {
|
|
6
|
-
const service = createGeminiTextService({ apiKey: '' });
|
|
5
|
+
test('createGeminiTextService retorna null no modo api_key quando GEMINI_API_KEY nao existe', () => {
|
|
6
|
+
const service = createGeminiTextService({ authMode: 'api_key', apiKey: '' });
|
|
7
7
|
assert.equal(service, null);
|
|
8
8
|
});
|
|
9
9
|
|
|
@@ -85,3 +85,60 @@ test('createGeminiTextService propaga erro detalhado da API', async (t) => {
|
|
|
85
85
|
/Modelo invalido/,
|
|
86
86
|
);
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
test('createGeminiTextService usa Gemini CLI quando authMode=cli', async () => {
|
|
90
|
+
const calls = [];
|
|
91
|
+
const service = createGeminiTextService({
|
|
92
|
+
authMode: 'cli',
|
|
93
|
+
cliCommand: 'gemini',
|
|
94
|
+
defaultModel: 'gemini-2.5-flash',
|
|
95
|
+
isCliAvailableImpl: () => true,
|
|
96
|
+
execFileAsyncImpl: async (file, args, options) => {
|
|
97
|
+
calls.push({ file, args, options });
|
|
98
|
+
return {
|
|
99
|
+
stdout: 'OK_GEMINI_CLI\n',
|
|
100
|
+
stderr: 'Loaded cached credentials.',
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assert.ok(service);
|
|
106
|
+
assert.equal(service.transport, 'cli');
|
|
107
|
+
|
|
108
|
+
const response = await service.generateText({
|
|
109
|
+
instructions: 'Responda curto.',
|
|
110
|
+
userPrompt: 'Diga OK.',
|
|
111
|
+
model: 'gemini-2.5-flash',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assert.equal(response.model, 'gemini-2.5-flash');
|
|
115
|
+
assert.equal(response.text, 'OK_GEMINI_CLI');
|
|
116
|
+
assert.equal(calls.length, 1);
|
|
117
|
+
assert.equal(calls[0].file, 'gemini');
|
|
118
|
+
assert.ok(calls[0].args.includes('-m'));
|
|
119
|
+
assert.ok(calls[0].args.includes('gemini-2.5-flash'));
|
|
120
|
+
assert.ok(calls[0].args.includes('-p'));
|
|
121
|
+
assert.ok(calls[0].args.includes('--output-format'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('createGeminiTextService propaga erro do Gemini CLI', async () => {
|
|
125
|
+
const service = createGeminiTextService({
|
|
126
|
+
authMode: 'cli',
|
|
127
|
+
cliCommand: 'gemini',
|
|
128
|
+
defaultModel: 'gemini-2.5-flash',
|
|
129
|
+
isCliAvailableImpl: () => true,
|
|
130
|
+
execFileAsyncImpl: async () => {
|
|
131
|
+
const error = new Error('Command failed');
|
|
132
|
+
error.stderr = 'ModelNotFoundError: Requested entity was not found.';
|
|
133
|
+
throw error;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await assert.rejects(
|
|
138
|
+
() =>
|
|
139
|
+
service.generateText({
|
|
140
|
+
userPrompt: 'teste',
|
|
141
|
+
}),
|
|
142
|
+
/ModelNotFoundError/,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import OpenAI from 'openai';
|
|
5
5
|
import { getAiHelpCachedResponse, upsertAiHelpCachedResponse } from './aiHelpResponseCacheRepository.js';
|
|
6
|
-
import { createGeminiTextService, DEFAULT_GEMINI_MODEL } from './geminiService.js';
|
|
6
|
+
import { createGeminiTextService, DEFAULT_GEMINI_MODEL, isGeminiAuthReady } from './geminiService.js';
|
|
7
7
|
|
|
8
8
|
const DEFAULT_FAQ_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
9
9
|
const DEFAULT_MAX_RESPONSE_CHARS = 3400;
|
|
@@ -169,6 +169,16 @@ const normalizeLlmProvider = (value, fallback = 'gemini') => {
|
|
|
169
169
|
return fallback;
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
+
const normalizeGeminiAuthMode = (value, fallback = 'cli') => {
|
|
173
|
+
const normalized = String(value || '')
|
|
174
|
+
.trim()
|
|
175
|
+
.toLowerCase();
|
|
176
|
+
if (normalized === 'api_key') return 'api_key';
|
|
177
|
+
if (normalized === 'cli') return 'cli';
|
|
178
|
+
if (normalized === 'auto') return 'auto';
|
|
179
|
+
return fallback;
|
|
180
|
+
};
|
|
181
|
+
|
|
172
182
|
const looksLikeGeminiModel = (value) =>
|
|
173
183
|
String(value || '')
|
|
174
184
|
.trim()
|
|
@@ -241,7 +251,13 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
|
|
|
241
251
|
const cachePathValue = String(faq.cache_file || '').trim();
|
|
242
252
|
const cachePath = cachePathValue ? path.resolve(process.cwd(), cachePathValue) : path.join(process.cwd(), 'data', 'cache', `${moduleKey}-ai-faq-cache.json`);
|
|
243
253
|
|
|
244
|
-
const
|
|
254
|
+
const geminiAuthMode = normalizeGeminiAuthMode(envValue('GEMINI_AUTH_MODE') || llm.gemini_auth_mode || process.env.GEMINI_AUTH_MODE, 'cli');
|
|
255
|
+
const hasGeminiAuthHint = isGeminiAuthReady({
|
|
256
|
+
authMode: geminiAuthMode,
|
|
257
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
258
|
+
cliCommand: process.env.GEMINI_CLI_COMMAND || 'gemini',
|
|
259
|
+
});
|
|
260
|
+
const provider = normalizeLlmProvider(envValue('PROVIDER') || llm.provider || process.env.AI_HELP_LLM_PROVIDER, hasGeminiAuthHint ? 'gemini' : 'openai');
|
|
245
261
|
const defaultModelByProvider = provider === 'gemini' ? process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL : process.env.OPENAI_MODEL || DEFAULT_OPENAI_MODEL;
|
|
246
262
|
const rawModel = String(envValue('MODEL') || llm.model || defaultModelByProvider).trim() || defaultModelByProvider;
|
|
247
263
|
const modelFromEnv = String(envValue('MODEL') || '').trim();
|
|
@@ -271,6 +287,7 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
|
|
|
271
287
|
.trim()
|
|
272
288
|
.toLowerCase() !== 'false',
|
|
273
289
|
provider,
|
|
290
|
+
geminiAuthMode,
|
|
274
291
|
model: resolvedModel,
|
|
275
292
|
maxResponseChars: Math.max(400, toPositiveInt(envValue('MAX_RESPONSE_CHARS') || llm.max_response_chars, DEFAULT_MAX_RESPONSE_CHARS, 400)),
|
|
276
293
|
maxAgentContextChars: Math.max(2_000, toPositiveInt(envValue('MAX_AGENT_CONTEXT_CHARS') || llm.max_agent_context_chars, DEFAULT_MAX_AGENT_CONTEXT_CHARS, 2_000)),
|
|
@@ -286,6 +303,7 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
|
|
|
286
303
|
let faqGenerationPromise = null;
|
|
287
304
|
let cachedOpenAIClient = null;
|
|
288
305
|
let cachedGeminiService = null;
|
|
306
|
+
let cachedGeminiServiceKey = '';
|
|
289
307
|
|
|
290
308
|
const createEmptyCache = () => ({
|
|
291
309
|
version: FAQ_CACHE_VERSION,
|
|
@@ -441,7 +459,14 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
|
|
|
441
459
|
.join('\n');
|
|
442
460
|
};
|
|
443
461
|
|
|
444
|
-
const isGeminiReady = () =>
|
|
462
|
+
const isGeminiReady = () => {
|
|
463
|
+
const config = getAiHelpConfig();
|
|
464
|
+
return isGeminiAuthReady({
|
|
465
|
+
authMode: config.llm.geminiAuthMode,
|
|
466
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
467
|
+
cliCommand: process.env.GEMINI_CLI_COMMAND || 'gemini',
|
|
468
|
+
});
|
|
469
|
+
};
|
|
445
470
|
const isOpenAIReady = () => Boolean(String(process.env.OPENAI_API_KEY || '').trim());
|
|
446
471
|
const isProviderReady = (provider) => (provider === 'gemini' ? isGeminiReady() : isOpenAIReady());
|
|
447
472
|
|
|
@@ -453,12 +478,16 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
|
|
|
453
478
|
const getGeminiService = () => {
|
|
454
479
|
if (!isGeminiReady()) return null;
|
|
455
480
|
const config = getAiHelpConfig();
|
|
456
|
-
|
|
481
|
+
const currentServiceKey = `${config.llm.model}|${config.llm.timeoutMs}|${config.llm.geminiAuthMode}|${process.env.GEMINI_CLI_COMMAND || 'gemini'}|${Boolean(String(process.env.GEMINI_API_KEY || '').trim())}`;
|
|
482
|
+
if (!cachedGeminiService || cachedGeminiServiceKey !== currentServiceKey) {
|
|
457
483
|
cachedGeminiService = createGeminiTextService({
|
|
458
484
|
apiKey: process.env.GEMINI_API_KEY,
|
|
459
485
|
defaultModel: config.llm.model || process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
|
|
460
486
|
timeoutMs: config.llm.timeoutMs,
|
|
487
|
+
authMode: config.llm.geminiAuthMode,
|
|
488
|
+
cliCommand: process.env.GEMINI_CLI_COMMAND || 'gemini',
|
|
461
489
|
});
|
|
490
|
+
cachedGeminiServiceKey = currentServiceKey;
|
|
462
491
|
}
|
|
463
492
|
return cachedGeminiService;
|
|
464
493
|
};
|
|
@@ -2,7 +2,7 @@ import logger from '#logger';
|
|
|
2
2
|
import { findById, upsert } from '../../../database/index.js';
|
|
3
3
|
import { extractUserIdInfo, resolveUserIdCached, isLidUserId, isWhatsAppUserId } from '../../config/index.js';
|
|
4
4
|
|
|
5
|
-
const GROUP_METADATA_FIELDS = ['id', 'subject', 'description', 'owner_jid', 'creation', 'participants'];
|
|
5
|
+
const GROUP_METADATA_FIELDS = ['id', 'subject', 'description', 'owner_jid', 'creation', 'participants', 'linked_parent_jid', 'is_community', 'is_community_announce', 'member_add_mode', 'join_approval_mode', 'addressing_mode'];
|
|
6
6
|
|
|
7
7
|
const PARTICIPANT_ACTIONS = new Set(['add', 'remove', 'promote', 'demote', 'modify']);
|
|
8
8
|
|
|
@@ -285,6 +285,17 @@ export const buildGroupMetadataFromUpdate = (event, existing) => {
|
|
|
285
285
|
const currentParticipants = parseParticipantsFromDb(existing?.participants);
|
|
286
286
|
const participants = event.participants || currentParticipants;
|
|
287
287
|
const normalizedParticipants = normalizeParticipantsList(participants);
|
|
288
|
+
const resolveExistingValue = (snakeKey, camelKey) => existing?.[snakeKey] ?? existing?.[camelKey];
|
|
289
|
+
const resolveBoolean = (eventValue, snakeKey, camelKey) => {
|
|
290
|
+
if (eventValue !== undefined) return Boolean(eventValue);
|
|
291
|
+
const existingValue = resolveExistingValue(snakeKey, camelKey);
|
|
292
|
+
return existingValue === undefined || existingValue === null ? null : Boolean(existingValue);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const linkedParentRaw = event.linkedParent ?? resolveExistingValue('linked_parent_jid', 'linkedParent');
|
|
296
|
+
const linkedParent = typeof linkedParentRaw === 'string' ? linkedParentRaw.trim() || null : linkedParentRaw || null;
|
|
297
|
+
const addressingModeRaw = event.addressingMode ?? resolveExistingValue('addressing_mode', 'addressingMode');
|
|
298
|
+
const addressingMode = typeof addressingModeRaw === 'string' ? addressingModeRaw.trim() || null : addressingModeRaw || null;
|
|
288
299
|
|
|
289
300
|
return {
|
|
290
301
|
id: event.id,
|
|
@@ -293,6 +304,12 @@ export const buildGroupMetadataFromUpdate = (event, existing) => {
|
|
|
293
304
|
owner_jid: event.owner ?? existing?.owner_jid,
|
|
294
305
|
creation: event.creation ?? existing?.creation,
|
|
295
306
|
participants: normalizedParticipants,
|
|
307
|
+
linked_parent_jid: linkedParent,
|
|
308
|
+
is_community: resolveBoolean(event.isCommunity, 'is_community', 'isCommunity'),
|
|
309
|
+
is_community_announce: resolveBoolean(event.isCommunityAnnounce, 'is_community_announce', 'isCommunityAnnounce'),
|
|
310
|
+
member_add_mode: resolveBoolean(event.memberAddMode, 'member_add_mode', 'memberAddMode'),
|
|
311
|
+
join_approval_mode: resolveBoolean(event.joinApprovalMode, 'join_approval_mode', 'joinApprovalMode'),
|
|
312
|
+
addressing_mode: addressingMode,
|
|
296
313
|
};
|
|
297
314
|
};
|
|
298
315
|
|
|
@@ -308,4 +325,10 @@ export const buildGroupMetadataFromGroup = (group) => ({
|
|
|
308
325
|
owner_jid: group.owner,
|
|
309
326
|
creation: group.creation,
|
|
310
327
|
participants: normalizeParticipantsList(group.participants || []),
|
|
328
|
+
linked_parent_jid: typeof group.linkedParent === 'string' ? group.linkedParent.trim() || null : group.linkedParent || null,
|
|
329
|
+
is_community: group.isCommunity === undefined ? null : Boolean(group.isCommunity),
|
|
330
|
+
is_community_announce: group.isCommunityAnnounce === undefined ? null : Boolean(group.isCommunityAnnounce),
|
|
331
|
+
member_add_mode: group.memberAddMode === undefined ? null : Boolean(group.memberAddMode),
|
|
332
|
+
join_approval_mode: group.joinApprovalMode === undefined ? null : Boolean(group.joinApprovalMode),
|
|
333
|
+
addressing_mode: typeof group.addressingMode === 'string' ? group.addressingMode.trim() || null : group.addressingMode || null,
|
|
311
334
|
});
|
|
@@ -105,7 +105,7 @@ const INVALID_SURROGATE_REGEX = /surrogate pair/i;
|
|
|
105
105
|
const messageQueue = [];
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* Conjunto de IDs de mensagens
|
|
108
|
+
* Conjunto de IDs de mensagens já enfileiradas no formato `${session_id}:${message_id}`.
|
|
109
109
|
* @type {Set<string>}
|
|
110
110
|
*/
|
|
111
111
|
const messagePendingIds = new Set();
|
|
@@ -132,7 +132,7 @@ const chatCache = new Map();
|
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Fila em memória com eventos do Baileys pendentes de persistência.
|
|
135
|
-
* @type {Array<{event_name:string, socket_generation:(number|null), chat_id:(string|null), message_id:(string|null), participant_id:(string|null), payload_summary:(string|null), event_timestamp:Date}>}
|
|
135
|
+
* @type {Array<{session_id:string, event_name:string, socket_generation:(number|null), chat_id:(string|null), message_id:(string|null), participant_id:(string|null), payload_summary:(string|null), event_timestamp:Date}>}
|
|
136
136
|
*/
|
|
137
137
|
const baileysEventQueue = [];
|
|
138
138
|
|
|
@@ -251,8 +251,8 @@ const isInvalidJsonPayloadError = (error) => {
|
|
|
251
251
|
* - content: remove surrogate inválido
|
|
252
252
|
* - raw_message: serializa JSON seguro para coluna JSON
|
|
253
253
|
*
|
|
254
|
-
* @param {{message_id:string, chat_id:string, sender_id:string, canonical_sender_id?:(string|null), content:(string|null), raw_message:(Object|string|null), timestamp:(number|string|Date)}} messageData
|
|
255
|
-
* @returns {{message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}}
|
|
254
|
+
* @param {{session_id?:(string|null), message_id:string, chat_id:string, sender_id:string, canonical_sender_id?:(string|null), content:(string|null), raw_message:(Object|string|null), timestamp:(number|string|Date), allow_group_write?:(boolean)}} messageData
|
|
255
|
+
* @returns {{session_id:string, message_id:string, chat_id:(string|null), sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}}
|
|
256
256
|
*/
|
|
257
257
|
const normalizeUserIdForColumn = (value, maxLength = 255) => {
|
|
258
258
|
if (value === null || value === undefined) return null;
|
|
@@ -277,8 +277,13 @@ const resolveCanonicalSenderIdForMessage = (messageData) => {
|
|
|
277
277
|
|
|
278
278
|
const normalizeMessageForQueue = (messageData) => {
|
|
279
279
|
const senderId = normalizeUserIdForColumn(messageData?.sender_id, 255);
|
|
280
|
+
const messageId = normalizeTextForColumn(messageData?.message_id, 255);
|
|
281
|
+
const chatId = normalizeTextForColumn(messageData?.chat_id, 255);
|
|
280
282
|
return {
|
|
281
283
|
...messageData,
|
|
284
|
+
session_id: normalizeSessionIdForColumn(messageData?.session_id || messageData?.sessionId),
|
|
285
|
+
message_id: messageId,
|
|
286
|
+
chat_id: chatId,
|
|
282
287
|
sender_id: senderId,
|
|
283
288
|
canonical_sender_id: resolveCanonicalSenderIdForMessage({
|
|
284
289
|
...messageData,
|
|
@@ -302,7 +307,25 @@ const normalizeTimestampForColumn = (value) => {
|
|
|
302
307
|
return __timeNow();
|
|
303
308
|
};
|
|
304
309
|
|
|
310
|
+
const normalizeSessionIdForColumn = (value) => {
|
|
311
|
+
const normalized = normalizeTextForColumn(value, 64);
|
|
312
|
+
return normalized || 'default';
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const isGroupChatId = (chatId) => {
|
|
316
|
+
const normalized = String(chatId || '').trim();
|
|
317
|
+
return normalized.endsWith('@g.us');
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const buildMessagePendingKey = (sessionId, messageId) => {
|
|
321
|
+
const safeSessionId = normalizeSessionIdForColumn(sessionId);
|
|
322
|
+
const safeMessageId = normalizeTextForColumn(messageId, 255);
|
|
323
|
+
if (!safeMessageId) return null;
|
|
324
|
+
return `${safeSessionId}:${safeMessageId}`;
|
|
325
|
+
};
|
|
326
|
+
|
|
305
327
|
const normalizeBaileysEventForQueue = (eventData) => ({
|
|
328
|
+
session_id: normalizeSessionIdForColumn(eventData?.session_id || eventData?.sessionId),
|
|
306
329
|
event_name: normalizeTextForColumn(eventData?.event_name, 64),
|
|
307
330
|
socket_generation: Number.isFinite(Number(eventData?.socket_generation)) ? Math.max(0, Math.floor(Number(eventData.socket_generation))) : null,
|
|
308
331
|
chat_id: normalizeTextForColumn(eventData?.chat_id, 255),
|
|
@@ -313,14 +336,14 @@ const normalizeBaileysEventForQueue = (eventData) => ({
|
|
|
313
336
|
});
|
|
314
337
|
|
|
315
338
|
const insertBaileysEventBatch = async (batch) => {
|
|
316
|
-
const placeholders = buildPlaceholders(batch.length,
|
|
339
|
+
const placeholders = buildPlaceholders(batch.length, 8);
|
|
317
340
|
const params = [];
|
|
318
341
|
for (const entry of batch) {
|
|
319
|
-
params.push(entry.event_name, entry.socket_generation, entry.chat_id, entry.message_id, entry.participant_id, entry.payload_summary, entry.event_timestamp);
|
|
342
|
+
params.push(entry.session_id, entry.event_name, entry.socket_generation, entry.chat_id, entry.message_id, entry.participant_id, entry.payload_summary, entry.event_timestamp);
|
|
320
343
|
}
|
|
321
344
|
|
|
322
345
|
const sql = `INSERT INTO ${TABLES.BAILEYS_EVENT_JOURNAL}
|
|
323
|
-
(event_name, socket_generation, chat_id, message_id, participant_id, payload_summary, event_timestamp)
|
|
346
|
+
(session_id, event_name, socket_generation, chat_id, message_id, participant_id, payload_summary, event_timestamp)
|
|
324
347
|
VALUES ${placeholders}`;
|
|
325
348
|
|
|
326
349
|
await executeQuery(sql, params);
|
|
@@ -329,18 +352,18 @@ const insertBaileysEventBatch = async (batch) => {
|
|
|
329
352
|
/**
|
|
330
353
|
* Executa INSERT IGNORE de um batch de mensagens.
|
|
331
354
|
*
|
|
332
|
-
* @param {Array<{message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
|
|
355
|
+
* @param {Array<{session_id:string, message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
|
|
333
356
|
* @returns {Promise<void>}
|
|
334
357
|
*/
|
|
335
358
|
const insertMessageBatch = async (batch) => {
|
|
336
|
-
const placeholders = buildPlaceholders(batch.length,
|
|
359
|
+
const placeholders = buildPlaceholders(batch.length, 8);
|
|
337
360
|
const params = [];
|
|
338
361
|
for (const message of batch) {
|
|
339
|
-
params.push(message.message_id, message.chat_id, message.sender_id, message.canonical_sender_id, message.content, message.raw_message, message.timestamp);
|
|
362
|
+
params.push(message.session_id, message.message_id, message.chat_id, message.sender_id, message.canonical_sender_id, message.content, message.raw_message, message.timestamp);
|
|
340
363
|
}
|
|
341
364
|
|
|
342
365
|
const sql = `INSERT IGNORE INTO ${TABLES.MESSAGES}
|
|
343
|
-
(message_id, chat_id, sender_id, canonical_sender_id, content, raw_message, timestamp)
|
|
366
|
+
(session_id, message_id, chat_id, sender_id, canonical_sender_id, content, raw_message, timestamp)
|
|
344
367
|
VALUES ${placeholders}`;
|
|
345
368
|
|
|
346
369
|
await executeQuery(sql, params);
|
|
@@ -424,12 +447,13 @@ const refreshMessageActivityDailyForBatch = async (batch) => {
|
|
|
424
447
|
/**
|
|
425
448
|
* Remove IDs de mensagens do set de pendentes.
|
|
426
449
|
*
|
|
427
|
-
* @param {Array<{message_id:string}>} batch
|
|
450
|
+
* @param {Array<{session_id:string, message_id:string}>} batch
|
|
428
451
|
* @returns {void}
|
|
429
452
|
*/
|
|
430
453
|
const clearPendingMessageIds = (batch) => {
|
|
431
454
|
for (const message of batch) {
|
|
432
|
-
|
|
455
|
+
const key = buildMessagePendingKey(message?.session_id, message?.message_id);
|
|
456
|
+
if (key) messagePendingIds.delete(key);
|
|
433
457
|
}
|
|
434
458
|
};
|
|
435
459
|
|
|
@@ -438,7 +462,7 @@ const clearPendingMessageIds = (batch) => {
|
|
|
438
462
|
* - Mensagem inválida é descartada para não travar a fila inteira.
|
|
439
463
|
* - Em erro transitório, re-enfileira o restante e interrompe.
|
|
440
464
|
*
|
|
441
|
-
* @param {Array<{message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
|
|
465
|
+
* @param {Array<{session_id:string, message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
|
|
442
466
|
* @returns {Promise<void>}
|
|
443
467
|
*/
|
|
444
468
|
const salvageJsonErrorBatch = async (batch) => {
|
|
@@ -679,26 +703,32 @@ const baileysEventFlushRunner = createFlushRunner({
|
|
|
679
703
|
|
|
680
704
|
/**
|
|
681
705
|
* Enfileira uma mensagem para INSERT no banco (INSERT IGNORE).
|
|
682
|
-
* - Evita duplicar message_id usando um Set.
|
|
706
|
+
* - Evita duplicar por `${session_id}:${message_id}` usando um Set.
|
|
683
707
|
* - Força flush se a fila estiver muito grande.
|
|
684
708
|
* - Agenda flush quando atinge o tamanho de batch.
|
|
685
709
|
*
|
|
686
|
-
* @param {{message_id:string, chat_id:string, sender_id:string, canonical_sender_id?:(string|null), content:(string|null), raw_message:(Object|string|null), timestamp:(number|string)}} messageData
|
|
710
|
+
* @param {{session_id?:(string|null), message_id:string, chat_id:string, sender_id:string, canonical_sender_id?:(string|null), content:(string|null), raw_message:(Object|string|null), timestamp:(number|string), allow_group_write?:(boolean)}} messageData
|
|
687
711
|
* Objeto com os campos necessários para persistência.
|
|
688
712
|
* @returns {boolean} true se foi enfileirada; false se inválida/duplicada.
|
|
689
713
|
*/
|
|
690
714
|
export function queueMessageInsert(messageData) {
|
|
691
|
-
if (!messageData?.message_id) return false;
|
|
692
|
-
if (messagePendingIds.has(messageData.message_id)) return false;
|
|
693
|
-
|
|
694
715
|
const normalizedMessage = normalizeMessageForQueue(messageData);
|
|
716
|
+
if (!normalizedMessage?.message_id) return false;
|
|
717
|
+
|
|
718
|
+
if (isGroupChatId(normalizedMessage.chat_id) && normalizedMessage.allow_group_write === false) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const pendingKey = buildMessagePendingKey(normalizedMessage.session_id, normalizedMessage.message_id);
|
|
723
|
+
if (!pendingKey) return false;
|
|
724
|
+
if (messagePendingIds.has(pendingKey)) return false;
|
|
695
725
|
|
|
696
726
|
if (messageQueue.length >= MESSAGE_QUEUE_MAX) {
|
|
697
727
|
logger.warn('Fila de mensagens cheia, forçando flush.', { size: messageQueue.length });
|
|
698
728
|
scheduleFlush();
|
|
699
729
|
}
|
|
700
730
|
|
|
701
|
-
messagePendingIds.add(
|
|
731
|
+
messagePendingIds.add(pendingKey);
|
|
702
732
|
messageQueue.push(normalizedMessage);
|
|
703
733
|
updateQueueMetrics();
|
|
704
734
|
|
|
@@ -782,7 +812,7 @@ export function queueChatUpdate(chat, options = {}) {
|
|
|
782
812
|
/**
|
|
783
813
|
* Enfileira um evento resumido do Baileys para o journal de auditoria.
|
|
784
814
|
*
|
|
785
|
-
* @param {{event_name:string, socket_generation?:(number|null), chat_id?:(string|null), message_id?:(string|null), participant_id?:(string|null), payload_summary?:any, event_timestamp?:(string|number|Date)}} eventData
|
|
815
|
+
* @param {{session_id?:(string|null), event_name:string, socket_generation?:(number|null), chat_id?:(string|null), message_id?:(string|null), participant_id?:(string|null), payload_summary?:any, event_timestamp?:(string|number|Date)}} eventData
|
|
786
816
|
* @returns {boolean}
|
|
787
817
|
*/
|
|
788
818
|
export function queueBaileysEventInsert(eventData) {
|