@omnizap-system/omnizap 2.6.2 → 2.6.3
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 +24 -0
- package/app/config/index.js +4 -0
- package/app/configParts/adminIdentity.js +29 -0
- package/app/configParts/baileysConfig.js +116 -0
- package/app/configParts/groupUtils.js +221 -0
- package/app/configParts/loggerConfig.js +185 -0
- package/app/configParts/messagePersistenceService.js +169 -7
- package/app/configParts/sessionConfig.js +85 -0
- package/app/connection/baileysCompatibility.test.js +9 -0
- package/app/connection/baileysDbAuthState.js +205 -9
- package/app/connection/baileysLibsignalPatch.js +210 -0
- package/app/connection/groupOwnerWriteStateResolver.js +53 -21
- package/app/connection/socketController.js +95 -25
- package/app/connection/socketController.multiSession.test.js +20 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +17 -3
- package/app/controllers/messageProcessingPipeline.js +2 -0
- package/app/controllers/messageProcessingPipeline.test.js +15 -13
- package/app/services/multiSession/assignmentBalancerService.js +1 -6
- package/app/services/multiSession/groupOwnershipRepository.js +9 -44
- package/app/services/multiSession/groupOwnershipService.js +9 -90
- package/app/services/multiSession/groupOwnershipService.test.js +12 -4
- package/app/services/multiSession/sessionRegistryService.js +6 -60
- package/app/utils/antiLink/antiLinkModule.js +54 -24
- package/docs/security/omnizap-static-security-headers.conf +3 -3
- package/package.json +3 -2
- package/public/comandos/commands-catalog.json +1 -1
- package/public/css/payments-react.css +478 -0
- package/public/js/apps/homeReactApp.js +2 -2
- package/public/js/apps/paymentsCancelReactApp.js +45 -0
- package/public/js/apps/paymentsReactApp.js +399 -0
- package/public/js/apps/paymentsSuccessReactApp.js +148 -0
- package/public/pages/pagamentos-cancelado.html +21 -0
- package/public/pages/pagamentos-sucesso.html +21 -0
- package/public/pages/pagamentos.html +30 -0
- package/scripts/deploy.sh +3 -0
- package/scripts/new-whatsapp-session.sh +247 -0
- package/server/controllers/admin/systemAdminController.js +4 -17
- package/server/controllers/payments/paymentsController.js +731 -0
- package/server/controllers/system/systemController.js +4 -30
- package/server/email/emailAutomationRuntime.js +36 -1
- package/server/email/emailAutomationService.js +42 -1
- package/server/email/emailTemplateService.js +137 -31
- package/server/http/httpRequestUtils.js +18 -14
- package/server/middleware/securityHeaders.js +15 -2
- package/server/routes/indexRouter.js +27 -7
- package/server/routes/payments/paymentsRouter.js +47 -0
- package/server/routes/static/staticPageRouter.js +3 -0
- package/vite.config.mjs +3 -0
|
@@ -1,30 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normaliza `assignmentVersion` para um inteiro positivo.
|
|
3
|
+
* @param {unknown} value
|
|
4
|
+
* @returns {number | null}
|
|
5
|
+
*/
|
|
1
6
|
export const normalizeAssignmentVersion = (value) => {
|
|
2
7
|
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
3
8
|
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
|
4
9
|
return parsed;
|
|
5
10
|
};
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{
|
|
14
|
+
* allowed: boolean,
|
|
15
|
+
* ownerSessionId: string | null,
|
|
16
|
+
* assignmentVersion: number | null,
|
|
17
|
+
* reason: string
|
|
18
|
+
* }} GroupOwnerWriteResolution
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{
|
|
22
|
+
* allowClaim?: boolean,
|
|
23
|
+
* bypassCache?: boolean,
|
|
24
|
+
* source?: string,
|
|
25
|
+
* expectedAssignmentVersion?: number | string | null,
|
|
26
|
+
* enforceFence?: boolean
|
|
27
|
+
* }} GroupOwnerWriteResolverOptions
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {{
|
|
31
|
+
* buildCacheKeyImpl: (groupJid: string, sessionId: string) => string,
|
|
32
|
+
* getOwnerImpl: (groupJid: string, options?: { bypassCache?: boolean }) => Promise<{ ownerSessionId?: string | null, assignmentVersion?: number | string | null } | null | undefined>,
|
|
33
|
+
* tryAcquireImpl: (options: {
|
|
34
|
+
* groupJid: string,
|
|
35
|
+
* sessionId: string,
|
|
36
|
+
* reason: string,
|
|
37
|
+
* changedBy: string,
|
|
38
|
+
* metadata?: Record<string, any>
|
|
39
|
+
* }) => Promise<{
|
|
40
|
+
* acquired?: boolean,
|
|
41
|
+
* reason?: string,
|
|
42
|
+
* assignmentVersion?: number | string | null,
|
|
43
|
+
* owner?: { ownerSessionId?: string | null, assignmentVersion?: number | string | null } | null
|
|
44
|
+
* } | null | undefined>,
|
|
45
|
+
* cacheImpl: { get: (key: string) => any, set: (key: string, value: any) => any },
|
|
46
|
+
* isGroupJidImpl: (jid: string) => boolean,
|
|
47
|
+
* normalizeSessionIdImpl: (sessionId: string | null | undefined) => string,
|
|
48
|
+
* loggerImpl: { warn: (message: string, meta?: Record<string, any>) => void },
|
|
49
|
+
* defaultAllowClaim?: boolean
|
|
50
|
+
* }} GroupOwnerWriteResolverDeps
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Cria um resolvedor de permissão de escrita por ownership de grupo.
|
|
54
|
+
* @param {GroupOwnerWriteResolverDeps} [deps]
|
|
55
|
+
* @returns {(groupJid: string, sessionId: string, options?: GroupOwnerWriteResolverOptions) => Promise<GroupOwnerWriteResolution>}
|
|
56
|
+
*/
|
|
57
|
+
export const createGroupOwnerWriteStateResolver =
|
|
58
|
+
({ buildCacheKeyImpl, getOwnerImpl, tryAcquireImpl, cacheImpl, isGroupJidImpl, normalizeSessionIdImpl, loggerImpl, defaultAllowClaim = true } = {}) =>
|
|
59
|
+
async (groupJid, sessionId, { allowClaim = defaultAllowClaim, bypassCache = false, source = 'unknown', expectedAssignmentVersion = null, enforceFence = true } = {}) => {
|
|
28
60
|
const safeGroupJid = String(groupJid || '').trim();
|
|
29
61
|
const safeSessionId = normalizeSessionIdImpl(sessionId);
|
|
30
62
|
if (!safeGroupJid || !isGroupJidImpl(safeGroupJid)) {
|
|
@@ -21,20 +21,19 @@ import { extractSenderInfoFromMessage, primeLidCache, resolveUserIdCached, isLid
|
|
|
21
21
|
import { queueBaileysEventInsert, queueChatUpdate, queueLidUpdate, queueMessageInsert } from '../services/infra/dbWriteQueue.js';
|
|
22
22
|
import { buildGroupMetadataFromGroup, buildGroupMetadataFromUpdate, upsertGroupMetadata, parseParticipantsFromDb } from '../services/group/groupMetadataService.js';
|
|
23
23
|
import { buildMessageData } from '../configParts/messagePersistenceService.js';
|
|
24
|
-
import {
|
|
25
|
-
getOwner as getGroupOwner,
|
|
26
|
-
tryAcquire as tryAcquireGroupOwner,
|
|
27
|
-
heartbeatOwnerSession as heartbeatGroupOwnerSession,
|
|
28
|
-
} from '../services/multiSession/groupOwnershipService.js';
|
|
24
|
+
import { getOwner as getGroupOwner, tryAcquire as tryAcquireGroupOwner, heartbeatOwnerSession as heartbeatGroupOwnerSession } from '../services/multiSession/groupOwnershipService.js';
|
|
29
25
|
import sessionRegistryService from '../services/multiSession/sessionRegistryService.js';
|
|
30
26
|
import { createGroupOwnerWriteStateResolver, normalizeAssignmentVersion } from './groupOwnerWriteStateResolver.js';
|
|
31
27
|
import { useDbAuthState } from './baileysDbAuthState.js';
|
|
28
|
+
import { applyLibsignalRuntimePatch } from './baileysLibsignalPatch.js';
|
|
32
29
|
|
|
33
30
|
import { fileURLToPath } from 'node:url';
|
|
34
31
|
|
|
35
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
36
33
|
const __dirname = path.dirname(__filename);
|
|
37
34
|
|
|
35
|
+
applyLibsignalRuntimePatch();
|
|
36
|
+
|
|
38
37
|
/**
|
|
39
38
|
* Indica se o ambiente de execução é de produção.
|
|
40
39
|
* @type {boolean}
|
|
@@ -139,13 +138,30 @@ const BAILEYS_GROUP_METADATA_CACHE_TTL_SECONDS = parseEnvInt(process.env.BAILEYS
|
|
|
139
138
|
*/
|
|
140
139
|
const BAILEYS_GROUP_METADATA_CACHE_CHECKPERIOD_SECONDS = parseEnvInt(process.env.BAILEYS_GROUP_METADATA_CACHE_CHECKPERIOD_SECONDS, 60, 10, 1800);
|
|
141
140
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
141
|
+
* Configuração de runtime para múltiplas sessões do Baileys.
|
|
142
|
+
* @type {{
|
|
143
|
+
* sessionIds?: string[],
|
|
144
|
+
* primarySessionId?: string,
|
|
145
|
+
* ownerLeaseMs?: number,
|
|
146
|
+
* ownerHeartbeatMs?: number,
|
|
147
|
+
* sessionWeights?: Record<string, number>
|
|
148
|
+
* }}
|
|
145
149
|
*/
|
|
146
150
|
const MULTI_SESSION_RUNTIME_CONFIG = getMultiSessionRuntimeConfig();
|
|
151
|
+
/**
|
|
152
|
+
* Lista imutável de IDs de sessão habilitadas.
|
|
153
|
+
* @type {readonly string[]}
|
|
154
|
+
*/
|
|
147
155
|
const BAILEYS_SESSION_IDS = Object.freeze(Array.isArray(MULTI_SESSION_RUNTIME_CONFIG?.sessionIds) && MULTI_SESSION_RUNTIME_CONFIG.sessionIds.length > 0 ? [...MULTI_SESSION_RUNTIME_CONFIG.sessionIds] : [String(process.env.BAILEYS_AUTH_SESSION_ID || 'default').trim() || 'default']);
|
|
156
|
+
/**
|
|
157
|
+
* Conjunto para consultas rápidas de sessão válida.
|
|
158
|
+
* @type {Set<string>}
|
|
159
|
+
*/
|
|
148
160
|
const BAILEYS_SESSION_ID_SET = new Set(BAILEYS_SESSION_IDS);
|
|
161
|
+
/**
|
|
162
|
+
* ID de sessão principal usado como fallback.
|
|
163
|
+
* @type {string}
|
|
164
|
+
*/
|
|
149
165
|
const BAILEYS_PRIMARY_SESSION_ID = String(MULTI_SESSION_RUNTIME_CONFIG?.primarySessionId || BAILEYS_SESSION_IDS[0] || 'default').trim() || 'default';
|
|
150
166
|
/**
|
|
151
167
|
* Habilita bootstrap inicial do auth state no MySQL usando os arquivos locais legados.
|
|
@@ -178,6 +194,11 @@ const BAILEYS_SINGLE_WRITER_LOCK_NAME_BASE = (() => {
|
|
|
178
194
|
return `omnizap:baileys:writer:${dbLabel}`;
|
|
179
195
|
})();
|
|
180
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Normaliza um ID de sessão, garantindo fallback para a sessão principal.
|
|
199
|
+
* @param {string | null | undefined} sessionId
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
181
202
|
const normalizeSessionId = (sessionId) => {
|
|
182
203
|
const normalized = String(sessionId || '').trim();
|
|
183
204
|
if (!normalized) return BAILEYS_PRIMARY_SESSION_ID;
|
|
@@ -185,6 +206,11 @@ const normalizeSessionId = (sessionId) => {
|
|
|
185
206
|
return normalized;
|
|
186
207
|
};
|
|
187
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Resolve o nome final do lock de escritor único para uma sessão.
|
|
211
|
+
* @param {string | null | undefined} sessionId
|
|
212
|
+
* @returns {string}
|
|
213
|
+
*/
|
|
188
214
|
const getWriterLockNameBySession = (sessionId) => {
|
|
189
215
|
const safeSessionId = normalizeSessionId(sessionId);
|
|
190
216
|
const base = BAILEYS_SINGLE_WRITER_LOCK_NAME_BASE;
|
|
@@ -194,29 +220,45 @@ const getWriterLockNameBySession = (sessionId) => {
|
|
|
194
220
|
return `${base}:${safeSessionId}`;
|
|
195
221
|
};
|
|
196
222
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
223
|
+
/**
|
|
224
|
+
* TTL do cache local de ownership de grupo (em milissegundos).
|
|
225
|
+
* @type {number}
|
|
226
|
+
*/
|
|
227
|
+
const GROUP_OWNER_WRITE_CACHE_TTL_MS = parseEnvInt(process.env.GROUP_OWNER_WRITE_CACHE_TTL_MS, Math.max(2_000, Math.floor((Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerHeartbeatMs) || 30_000) / 3)), 1_000, 60_000);
|
|
228
|
+
/**
|
|
229
|
+
* Permite tentar reivindicar ownership no miss de cache.
|
|
230
|
+
* @type {boolean}
|
|
231
|
+
*/
|
|
203
232
|
const GROUP_OWNER_WRITE_CLAIM_ON_MISS = parseEnvBool(process.env.GROUP_OWNER_WRITE_CLAIM_ON_MISS, true);
|
|
233
|
+
/**
|
|
234
|
+
* Duração da lease de ownership por sessão (em milissegundos).
|
|
235
|
+
* @type {number}
|
|
236
|
+
*/
|
|
204
237
|
const GROUP_OWNER_LEASE_MS = Math.max(5_000, Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerLeaseMs) || 120_000);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
);
|
|
238
|
+
/**
|
|
239
|
+
* Intervalo de heartbeat de ownership (em milissegundos).
|
|
240
|
+
* @type {number}
|
|
241
|
+
*/
|
|
242
|
+
let GROUP_OWNER_HEARTBEAT_MS = parseEnvInt(process.env.GROUP_OWNER_HEARTBEAT_RUNTIME_MS, Math.max(1_000, Math.min(GROUP_OWNER_LEASE_MS - 500, Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerHeartbeatMs) || 30_000)), 1_000, 5 * 60 * 1000);
|
|
211
243
|
if (GROUP_OWNER_HEARTBEAT_MS >= GROUP_OWNER_LEASE_MS) {
|
|
212
244
|
GROUP_OWNER_HEARTBEAT_MS = Math.max(1_000, Math.floor(GROUP_OWNER_LEASE_MS / 2));
|
|
213
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Cache local de decisões de escrita por ownership de grupo.
|
|
248
|
+
* @type {NodeCache}
|
|
249
|
+
*/
|
|
214
250
|
const groupOwnerWriteStateCache = new NodeCache({
|
|
215
251
|
stdTTL: Math.max(1, Math.ceil(GROUP_OWNER_WRITE_CACHE_TTL_MS / 1000)),
|
|
216
252
|
checkperiod: Math.max(1, Math.ceil(GROUP_OWNER_WRITE_CACHE_TTL_MS / 1000)),
|
|
217
253
|
useClones: false,
|
|
218
254
|
});
|
|
219
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Monta a chave de cache de ownership para uma combinação sessão+grupo.
|
|
258
|
+
* @param {string | null | undefined} groupJid
|
|
259
|
+
* @param {string | null | undefined} sessionId
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
220
262
|
const buildGroupOwnerWriteCacheKey = (groupJid, sessionId) => {
|
|
221
263
|
const safeGroupJid = String(groupJid || '').trim();
|
|
222
264
|
const safeSessionId = normalizeSessionId(sessionId);
|
|
@@ -224,6 +266,11 @@ const buildGroupOwnerWriteCacheKey = (groupJid, sessionId) => {
|
|
|
224
266
|
return `${safeSessionId}:${safeGroupJid}`;
|
|
225
267
|
};
|
|
226
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Remove do cache todas as entradas de ownership da sessão informada.
|
|
271
|
+
* @param {string | null | undefined} sessionId
|
|
272
|
+
* @returns {void}
|
|
273
|
+
*/
|
|
227
274
|
const clearGroupOwnerWriteCacheForSession = (sessionId) => {
|
|
228
275
|
const safeSessionId = normalizeSessionId(sessionId);
|
|
229
276
|
const prefix = `${safeSessionId}:`;
|
|
@@ -362,6 +409,11 @@ let activeSocket = null;
|
|
|
362
409
|
*/
|
|
363
410
|
const sessionContexts = new Map();
|
|
364
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Cria o contexto de runtime inicial para uma sessão.
|
|
414
|
+
* @param {string} sessionId
|
|
415
|
+
* @returns {SessionContext}
|
|
416
|
+
*/
|
|
365
417
|
const createSessionContext = (sessionId) => ({
|
|
366
418
|
sessionId,
|
|
367
419
|
socket: null,
|
|
@@ -375,6 +427,12 @@ const createSessionContext = (sessionId) => ({
|
|
|
375
427
|
ownerHeartbeatInFlight: false,
|
|
376
428
|
});
|
|
377
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Obtém o contexto de runtime de uma sessão.
|
|
432
|
+
* @param {string | null | undefined} sessionId
|
|
433
|
+
* @param {{ createIfMissing?: boolean }} [options]
|
|
434
|
+
* @returns {SessionContext | null}
|
|
435
|
+
*/
|
|
378
436
|
const getSessionContext = (sessionId, { createIfMissing = true } = {}) => {
|
|
379
437
|
const safeSessionId = normalizeSessionId(sessionId);
|
|
380
438
|
let context = sessionContexts.get(safeSessionId);
|
|
@@ -385,6 +443,11 @@ const getSessionContext = (sessionId, { createIfMissing = true } = {}) => {
|
|
|
385
443
|
return context || null;
|
|
386
444
|
};
|
|
387
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Resolve qual socket deve ser exposto como "ativo" no runtime legado.
|
|
448
|
+
* Prioriza a sessão principal quando conectada.
|
|
449
|
+
* @returns {import('@whiskeysockets/baileys').WASocket | null}
|
|
450
|
+
*/
|
|
388
451
|
const resolvePreferredActiveSocket = () => {
|
|
389
452
|
const primaryContext = getSessionContext(BAILEYS_PRIMARY_SESSION_ID, { createIfMissing: false });
|
|
390
453
|
if (isSocketOpen(primaryContext?.socket)) return primaryContext.socket;
|
|
@@ -396,6 +459,10 @@ const resolvePreferredActiveSocket = () => {
|
|
|
396
459
|
return primaryContext?.socket || null;
|
|
397
460
|
};
|
|
398
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Sincroniza a referência legada `activeSocket` com os contextos por sessão.
|
|
464
|
+
* @returns {void}
|
|
465
|
+
*/
|
|
399
466
|
const syncLegacyActiveSocketReference = () => {
|
|
400
467
|
activeSocket = resolvePreferredActiveSocket();
|
|
401
468
|
};
|
|
@@ -1373,8 +1440,7 @@ const startGroupOwnerHeartbeat = (sessionId, generation) => {
|
|
|
1373
1440
|
latestContext.ownerHeartbeatInFlight = true;
|
|
1374
1441
|
try {
|
|
1375
1442
|
const socket = latestContext.socket;
|
|
1376
|
-
const botJid =
|
|
1377
|
-
normalizeJid(socket?.user?.id || socket?.authState?.creds?.me?.id || socket?.authState?.creds?.me?.lid) || undefined;
|
|
1443
|
+
const botJid = normalizeJid(socket?.user?.id || socket?.authState?.creds?.me?.id || socket?.authState?.creds?.me?.lid) || undefined;
|
|
1378
1444
|
const sessionWeight = Math.max(1, Number(MULTI_SESSION_RUNTIME_CONFIG?.sessionWeights?.[safeSessionId] || 1));
|
|
1379
1445
|
const heartbeatOutcome = await heartbeatGroupOwnerSession({
|
|
1380
1446
|
sessionId: safeSessionId,
|
|
@@ -1532,6 +1598,11 @@ const ensureBaileysWriterLock = async (sessionId) => {
|
|
|
1532
1598
|
}
|
|
1533
1599
|
};
|
|
1534
1600
|
|
|
1601
|
+
/**
|
|
1602
|
+
* Libera todos os locks de escritor mantidos pelo processo atual.
|
|
1603
|
+
* @param {string} [reason='unknown']
|
|
1604
|
+
* @returns {Promise<void>}
|
|
1605
|
+
*/
|
|
1535
1606
|
const releaseAllBaileysWriterLocks = async (reason = 'unknown') => {
|
|
1536
1607
|
const targets = Array.from(sessionContexts.keys());
|
|
1537
1608
|
if (!targets.length) {
|
|
@@ -1634,6 +1705,7 @@ const syncGroupsOnConnectionOpen = async (sock) => {
|
|
|
1634
1705
|
* Configura autenticação, cria o socket e registra handlers de eventos.
|
|
1635
1706
|
* Gerencia a lógica de reconexão e a distribuição de eventos.
|
|
1636
1707
|
* @async
|
|
1708
|
+
* @param {string} [sessionId=BAILEYS_PRIMARY_SESSION_ID] - Sessão alvo da conexão.
|
|
1637
1709
|
* @returns {Promise<void>} Conclusão da inicialização e do registro de handlers.
|
|
1638
1710
|
* @throws {Error} Lança erro se a conexão inicial falhar.
|
|
1639
1711
|
*/
|
|
@@ -2103,9 +2175,7 @@ export async function connectToWhatsApp(sessionId = BAILEYS_PRIMARY_SESSION_ID)
|
|
|
2103
2175
|
*/
|
|
2104
2176
|
export async function connectAllWhatsAppSessions() {
|
|
2105
2177
|
const results = await Promise.allSettled(BAILEYS_SESSION_IDS.map((sessionId) => connectToWhatsApp(sessionId)));
|
|
2106
|
-
const failures = results
|
|
2107
|
-
.map((result, index) => ({ result, sessionId: BAILEYS_SESSION_IDS[index] }))
|
|
2108
|
-
.filter(({ result }) => result.status === 'rejected');
|
|
2178
|
+
const failures = results.map((result, index) => ({ result, sessionId: BAILEYS_SESSION_IDS[index] })).filter(({ result }) => result.status === 'rejected');
|
|
2109
2179
|
|
|
2110
2180
|
if (failures.length > 0) {
|
|
2111
2181
|
const error = new Error(`Falha ao conectar ${failures.length}/${BAILEYS_SESSION_IDS.length} sessões do WhatsApp.`);
|
|
@@ -3,6 +3,10 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
|
|
4
4
|
import { createGroupOwnerWriteStateResolver } from './groupOwnerWriteStateResolver.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Cria um cache em memória simples para testes.
|
|
8
|
+
* @returns {{get: (key: string) => any, set: (key: string, value: any) => boolean, del: (key: string) => boolean, keys: () => string[]}}
|
|
9
|
+
*/
|
|
6
10
|
const createCache = () => {
|
|
7
11
|
const map = new Map();
|
|
8
12
|
return {
|
|
@@ -16,9 +20,25 @@ const createCache = () => {
|
|
|
16
20
|
};
|
|
17
21
|
};
|
|
18
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Monta a chave de cache por sessão e grupo.
|
|
25
|
+
* @param {string} groupJid
|
|
26
|
+
* @param {string} sessionId
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
19
29
|
const buildCacheKey = (groupJid, sessionId) => `${sessionId}:${groupJid}`;
|
|
20
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Normaliza o ID da sessão para os cenários de teste.
|
|
33
|
+
* @param {string | null | undefined} value
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
21
36
|
const normalizeSessionId = (value) => String(value || '').trim() || 'default';
|
|
37
|
+
/**
|
|
38
|
+
* Verifica se o JID pertence a grupo.
|
|
39
|
+
* @param {string | null | undefined} jid
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
22
42
|
const isGroupJid = (jid) => String(jid || '').endsWith('@g.us');
|
|
23
43
|
|
|
24
44
|
test('socketController multi-session: fencing token por assignment_version invalida writer stale', async () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJid, stopMessagePipeline, handleAntiLink, ensureCommandPrefixForContext, resolveCaptchaByMessage, maybeHandleStartLoginMessage, mergeAnalysisMetadata, ensureGroupConfigForContext, resolveStickerFocusState, resolveStickerFocusMessageClassification, resolveGroupOwnerForContext, ownerEnforcementMode = 'off', primarySessionId = 'default', resolveSenderAdminForContext, isUserAdmin, canSendMessageInStickerFocus, registerMessageUsageInStickerFocus, shouldSendStickerFocusWarning, sendReply, formatStickerFocusRuleLabel, formatRemainingMinutesLabel, logger }) => {
|
|
1
|
+
export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJid, stopMessagePipeline, handleAntiLink, ensureCommandPrefixForContext, resolveCaptchaByMessage, maybeHandleStartLoginMessage, mergeAnalysisMetadata, ensureGroupConfigForContext, resolveStickerFocusState, resolveStickerFocusMessageClassification, resolveGroupOwnerForContext, ownerEnforcementMode = 'off', primarySessionId = 'default', allowSelfCommandsOnAppend = true, resolveSenderAdminForContext, isUserAdmin, canSendMessageInStickerFocus, registerMessageUsageInStickerFocus, shouldSendStickerFocusWarning, sendReply, formatStickerFocusRuleLabel, formatRemainingMinutesLabel, logger }) => {
|
|
2
2
|
const normalizedOwnerEnforcementMode = String(ownerEnforcementMode || 'off')
|
|
3
3
|
.trim()
|
|
4
4
|
.toLowerCase();
|
|
@@ -156,12 +156,26 @@ export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJ
|
|
|
156
156
|
|
|
157
157
|
const detectCommandIntentMiddleware = async (ctx) => {
|
|
158
158
|
ctx.hasCommandPrefix = ctx.extractedText.startsWith(ctx.commandPrefix);
|
|
159
|
-
|
|
159
|
+
const isSelfAppendCommand =
|
|
160
|
+
allowSelfCommandsOnAppend &&
|
|
161
|
+
ctx.hasCommandPrefix &&
|
|
162
|
+
!ctx.isNotifyUpsert &&
|
|
163
|
+
String(ctx.upsertType || '')
|
|
164
|
+
.trim()
|
|
165
|
+
.toLowerCase() === 'append' &&
|
|
166
|
+
ctx.isMessageFromBot;
|
|
167
|
+
ctx.isCommandMessage = ctx.hasCommandPrefix && (ctx.isNotifyUpsert || isSelfAppendCommand);
|
|
160
168
|
|
|
161
169
|
ctx.analysisPayload.isCommand = ctx.isCommandMessage;
|
|
162
170
|
ctx.analysisPayload.commandPrefix = ctx.commandPrefix;
|
|
163
171
|
|
|
164
|
-
if (
|
|
172
|
+
if (isSelfAppendCommand) {
|
|
173
|
+
mergeAnalysisMetadata(ctx.analysisPayload, {
|
|
174
|
+
command_detected_via: 'append_from_me',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (ctx.hasCommandPrefix && !ctx.isCommandMessage) {
|
|
165
179
|
mergeAnalysisMetadata(ctx.analysisPayload, {
|
|
166
180
|
command_suppressed_reason: 'non_notify_upsert',
|
|
167
181
|
});
|
|
@@ -44,6 +44,7 @@ const MESSAGE_REPLY_PRESENCE_DELAY_MS = parseEnvInt(process.env.MESSAGE_REPLY_PR
|
|
|
44
44
|
const MESSAGE_REPLY_PRESENCE_SUBSCRIBE = parseEnvBool(process.env.MESSAGE_REPLY_PRESENCE_SUBSCRIBE, true);
|
|
45
45
|
const CONVERSATIONAL_AUTO_REPLY_ENABLED = parseEnvBool(process.env.CONVERSATIONAL_AUTO_REPLY_ENABLED, false);
|
|
46
46
|
const WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN = parseEnvBool(process.env.WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN, true);
|
|
47
|
+
const WHATSAPP_ALLOW_SELF_COMMANDS_ON_APPEND = parseEnvBool(process.env.WHATSAPP_ALLOW_SELF_COMMANDS_ON_APPEND, true);
|
|
47
48
|
const SITE_ORIGIN =
|
|
48
49
|
String(process.env.SITE_ORIGIN || process.env.WHATSAPP_LOGIN_BASE_URL || 'https://omnizap.shop')
|
|
49
50
|
.trim()
|
|
@@ -638,6 +639,7 @@ const { touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, enf
|
|
|
638
639
|
resolveGroupOwnerForContext,
|
|
639
640
|
ownerEnforcementMode: GROUP_OWNER_ENFORCEMENT_MODE,
|
|
640
641
|
primarySessionId: PRIMARY_SESSION_ID,
|
|
642
|
+
allowSelfCommandsOnAppend: WHATSAPP_ALLOW_SELF_COMMANDS_ON_APPEND,
|
|
641
643
|
resolveSenderAdminForContext,
|
|
642
644
|
isUserAdmin,
|
|
643
645
|
canSendMessageInStickerFocus,
|
|
@@ -39,19 +39,21 @@ const createContext = (overrides = {}) => ({
|
|
|
39
39
|
...overrides,
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
const createStopMessagePipeline =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
const createStopMessagePipeline =
|
|
43
|
+
() =>
|
|
44
|
+
(ctx, processingResult = '', metadataPatch = null) => {
|
|
45
|
+
if (processingResult) {
|
|
46
|
+
ctx.analysisPayload.processingResult = processingResult;
|
|
47
|
+
}
|
|
48
|
+
if (metadataPatch) {
|
|
49
|
+
ctx.analysisPayload.metadata = {
|
|
50
|
+
...(ctx.analysisPayload.metadata || {}),
|
|
51
|
+
...(metadataPatch || {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
ctx.pipelineStopped = true;
|
|
55
|
+
return { stop: true };
|
|
56
|
+
};
|
|
55
57
|
|
|
56
58
|
const mergeAnalysisMetadata = (analysisPayload, patch) => {
|
|
57
59
|
analysisPayload.metadata = {
|
|
@@ -282,12 +282,7 @@ export const runGroupAssignmentBalancerCycle = async () => {
|
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
const [groupCounts, messageRates, errorCounts, assignments] = await Promise.all([
|
|
286
|
-
fetchGroupCountsBySession(onlineSessionIds),
|
|
287
|
-
fetchMessagesPerMinuteBySession(onlineSessionIds),
|
|
288
|
-
fetchRecentErrorsBySession(onlineSessionIds),
|
|
289
|
-
fetchCandidateAssignments(onlineSessionIds),
|
|
290
|
-
]);
|
|
285
|
+
const [groupCounts, messageRates, errorCounts, assignments] = await Promise.all([fetchGroupCountsBySession(onlineSessionIds), fetchMessagesPerMinuteBySession(onlineSessionIds), fetchRecentErrorsBySession(onlineSessionIds), fetchCandidateAssignments(onlineSessionIds)]);
|
|
291
286
|
|
|
292
287
|
const nowMs = Date.now();
|
|
293
288
|
const sessionStats = new Map();
|
|
@@ -63,9 +63,7 @@ export const normalizeSessionId = (value) => {
|
|
|
63
63
|
|
|
64
64
|
export const normalizeReason = (value) => {
|
|
65
65
|
if (value === undefined || value === null) return null;
|
|
66
|
-
const normalized = String(value)
|
|
67
|
-
.trim()
|
|
68
|
-
.slice(0, MAX_REASON_LENGTH);
|
|
66
|
+
const normalized = String(value).trim().slice(0, MAX_REASON_LENGTH);
|
|
69
67
|
return normalized || null;
|
|
70
68
|
};
|
|
71
69
|
|
|
@@ -143,15 +141,7 @@ export const getAssignmentForUpdate = async (groupJid, connection) => {
|
|
|
143
141
|
return normalizeAssignmentRow(rows?.[0] || null);
|
|
144
142
|
};
|
|
145
143
|
|
|
146
|
-
export const listAssignments = async (
|
|
147
|
-
{
|
|
148
|
-
groupJid = null,
|
|
149
|
-
ownerSessionId = null,
|
|
150
|
-
includeExpired = true,
|
|
151
|
-
limit = 200,
|
|
152
|
-
} = {},
|
|
153
|
-
connection = null,
|
|
154
|
-
) => {
|
|
144
|
+
export const listAssignments = async ({ groupJid = null, ownerSessionId = null, includeExpired = true, limit = 200 } = {}, connection = null) => {
|
|
155
145
|
const safeGroupJid = normalizeGroupJid(groupJid);
|
|
156
146
|
const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
|
|
157
147
|
const safeLimit = clampLimit(limit, 200, 1, 5_000);
|
|
@@ -184,15 +174,10 @@ export const listAssignments = async (
|
|
|
184
174
|
connection,
|
|
185
175
|
);
|
|
186
176
|
|
|
187
|
-
return (Array.isArray(rows) ? rows : [])
|
|
188
|
-
.map((row) => normalizeAssignmentRow(row))
|
|
189
|
-
.filter(Boolean);
|
|
177
|
+
return (Array.isArray(rows) ? rows : []).map((row) => normalizeAssignmentRow(row)).filter(Boolean);
|
|
190
178
|
};
|
|
191
179
|
|
|
192
|
-
export const createAssignment = async (
|
|
193
|
-
{ groupJid, ownerSessionId, leaseExpiresAt, cooldownUntil = null, pinned = false, reason = null, assignmentVersion = 1 } = {},
|
|
194
|
-
connection = null,
|
|
195
|
-
) => {
|
|
180
|
+
export const createAssignment = async ({ groupJid, ownerSessionId, leaseExpiresAt, cooldownUntil = null, pinned = false, reason = null, assignmentVersion = 1 } = {}, connection = null) => {
|
|
196
181
|
const safeGroupJid = normalizeGroupJid(groupJid);
|
|
197
182
|
const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
|
|
198
183
|
const safeLeaseExpiresAt = toDateOrNull(leaseExpiresAt);
|
|
@@ -211,10 +196,7 @@ export const createAssignment = async (
|
|
|
211
196
|
return getAssignment(safeGroupJid, connection);
|
|
212
197
|
};
|
|
213
198
|
|
|
214
|
-
export const updateAssignmentOwner = async (
|
|
215
|
-
{ groupJid, ownerSessionId, leaseExpiresAt, reason = null, bumpVersion = true, cooldownUntil = undefined, pinned = undefined } = {},
|
|
216
|
-
connection = null,
|
|
217
|
-
) => {
|
|
199
|
+
export const updateAssignmentOwner = async ({ groupJid, ownerSessionId, leaseExpiresAt, reason = null, bumpVersion = true, cooldownUntil = undefined, pinned = undefined } = {}, connection = null) => {
|
|
218
200
|
const safeGroupJid = normalizeGroupJid(groupJid);
|
|
219
201
|
const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
|
|
220
202
|
const safeLeaseExpiresAt = toDateOrNull(leaseExpiresAt);
|
|
@@ -252,10 +234,7 @@ export const updateAssignmentOwner = async (
|
|
|
252
234
|
return getAssignment(safeGroupJid, connection);
|
|
253
235
|
};
|
|
254
236
|
|
|
255
|
-
export const updateAssignmentLease = async (
|
|
256
|
-
{ groupJid, ownerSessionId, leaseExpiresAt, reason = undefined } = {},
|
|
257
|
-
connection = null,
|
|
258
|
-
) => {
|
|
237
|
+
export const updateAssignmentLease = async ({ groupJid, ownerSessionId, leaseExpiresAt, reason = undefined } = {}, connection = null) => {
|
|
259
238
|
const safeGroupJid = normalizeGroupJid(groupJid);
|
|
260
239
|
const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
|
|
261
240
|
const safeLeaseExpiresAt = toDateOrNull(leaseExpiresAt);
|
|
@@ -283,10 +262,7 @@ export const updateAssignmentLease = async (
|
|
|
283
262
|
return getAssignment(safeGroupJid, connection);
|
|
284
263
|
};
|
|
285
264
|
|
|
286
|
-
export const expireAssignment = async (
|
|
287
|
-
{ groupJid, ownerSessionId = null, reason = null, bumpVersion = true, leaseExpiresAt = new Date() } = {},
|
|
288
|
-
connection = null,
|
|
289
|
-
) => {
|
|
265
|
+
export const expireAssignment = async ({ groupJid, ownerSessionId = null, reason = null, bumpVersion = true, leaseExpiresAt = new Date() } = {}, connection = null) => {
|
|
290
266
|
const safeGroupJid = normalizeGroupJid(groupJid);
|
|
291
267
|
if (!safeGroupJid) {
|
|
292
268
|
throw new Error('expireAssignment requer groupJid valido.');
|
|
@@ -317,15 +293,7 @@ export const expireAssignment = async (
|
|
|
317
293
|
return getAssignment(safeGroupJid, connection);
|
|
318
294
|
};
|
|
319
295
|
|
|
320
|
-
export const renewLeasesByOwner = async (
|
|
321
|
-
{
|
|
322
|
-
ownerSessionId,
|
|
323
|
-
leaseExpiresAt,
|
|
324
|
-
reason = null,
|
|
325
|
-
now = undefined,
|
|
326
|
-
} = {},
|
|
327
|
-
connection = null,
|
|
328
|
-
) => {
|
|
296
|
+
export const renewLeasesByOwner = async ({ ownerSessionId, leaseExpiresAt, reason = null, now = undefined } = {}, connection = null) => {
|
|
329
297
|
const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
|
|
330
298
|
const safeLeaseExpiresAt = toDateOrNull(leaseExpiresAt);
|
|
331
299
|
const safeNow = now === undefined ? new Date() : toDateOrNull(now) || new Date();
|
|
@@ -346,10 +314,7 @@ export const renewLeasesByOwner = async (
|
|
|
346
314
|
return Number(result?.affectedRows || 0);
|
|
347
315
|
};
|
|
348
316
|
|
|
349
|
-
export const insertAssignmentHistory = async (
|
|
350
|
-
{ groupJid, previousSessionId = null, newSessionId, changeReason = null, changedBy = 'system', assignmentVersion = 1, metadata = null } = {},
|
|
351
|
-
connection = null,
|
|
352
|
-
) => {
|
|
317
|
+
export const insertAssignmentHistory = async ({ groupJid, previousSessionId = null, newSessionId, changeReason = null, changedBy = 'system', assignmentVersion = 1, metadata = null } = {}, connection = null) => {
|
|
353
318
|
const safeGroupJid = normalizeGroupJid(groupJid);
|
|
354
319
|
const safePreviousSessionId = normalizeSessionId(previousSessionId);
|
|
355
320
|
const safeNewSessionId = normalizeSessionId(newSessionId) || safePreviousSessionId;
|