@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.
Files changed (48) hide show
  1. package/.env.example +24 -0
  2. package/app/config/index.js +4 -0
  3. package/app/configParts/adminIdentity.js +29 -0
  4. package/app/configParts/baileysConfig.js +116 -0
  5. package/app/configParts/groupUtils.js +221 -0
  6. package/app/configParts/loggerConfig.js +185 -0
  7. package/app/configParts/messagePersistenceService.js +169 -7
  8. package/app/configParts/sessionConfig.js +85 -0
  9. package/app/connection/baileysCompatibility.test.js +9 -0
  10. package/app/connection/baileysDbAuthState.js +205 -9
  11. package/app/connection/baileysLibsignalPatch.js +210 -0
  12. package/app/connection/groupOwnerWriteStateResolver.js +53 -21
  13. package/app/connection/socketController.js +95 -25
  14. package/app/connection/socketController.multiSession.test.js +20 -0
  15. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +17 -3
  16. package/app/controllers/messageProcessingPipeline.js +2 -0
  17. package/app/controllers/messageProcessingPipeline.test.js +15 -13
  18. package/app/services/multiSession/assignmentBalancerService.js +1 -6
  19. package/app/services/multiSession/groupOwnershipRepository.js +9 -44
  20. package/app/services/multiSession/groupOwnershipService.js +9 -90
  21. package/app/services/multiSession/groupOwnershipService.test.js +12 -4
  22. package/app/services/multiSession/sessionRegistryService.js +6 -60
  23. package/app/utils/antiLink/antiLinkModule.js +54 -24
  24. package/docs/security/omnizap-static-security-headers.conf +3 -3
  25. package/package.json +3 -2
  26. package/public/comandos/commands-catalog.json +1 -1
  27. package/public/css/payments-react.css +478 -0
  28. package/public/js/apps/homeReactApp.js +2 -2
  29. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  30. package/public/js/apps/paymentsReactApp.js +399 -0
  31. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  32. package/public/pages/pagamentos-cancelado.html +21 -0
  33. package/public/pages/pagamentos-sucesso.html +21 -0
  34. package/public/pages/pagamentos.html +30 -0
  35. package/scripts/deploy.sh +3 -0
  36. package/scripts/new-whatsapp-session.sh +247 -0
  37. package/server/controllers/admin/systemAdminController.js +4 -17
  38. package/server/controllers/payments/paymentsController.js +731 -0
  39. package/server/controllers/system/systemController.js +4 -30
  40. package/server/email/emailAutomationRuntime.js +36 -1
  41. package/server/email/emailAutomationService.js +42 -1
  42. package/server/email/emailTemplateService.js +137 -31
  43. package/server/http/httpRequestUtils.js +18 -14
  44. package/server/middleware/securityHeaders.js +15 -2
  45. package/server/routes/indexRouter.js +27 -7
  46. package/server/routes/payments/paymentsRouter.js +47 -0
  47. package/server/routes/static/staticPageRouter.js +3 -0
  48. 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
- export const createGroupOwnerWriteStateResolver = ({
8
- buildCacheKeyImpl,
9
- getOwnerImpl,
10
- tryAcquireImpl,
11
- cacheImpl,
12
- isGroupJidImpl,
13
- normalizeSessionIdImpl,
14
- loggerImpl,
15
- defaultAllowClaim = true,
16
- } = {}) =>
17
- async (
18
- groupJid,
19
- sessionId,
20
- {
21
- allowClaim = defaultAllowClaim,
22
- bypassCache = false,
23
- source = 'unknown',
24
- expectedAssignmentVersion = null,
25
- enforceFence = true,
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
- * Identificador lógico da sessão de autenticação do Baileys no MySQL.
143
- * Permite isolar múltiplas sessões no mesmo banco.
144
- * @type {string}
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
- const GROUP_OWNER_WRITE_CACHE_TTL_MS = parseEnvInt(
198
- process.env.GROUP_OWNER_WRITE_CACHE_TTL_MS,
199
- Math.max(2_000, Math.floor((Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerHeartbeatMs) || 30_000) / 3)),
200
- 1_000,
201
- 60_000,
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
- let GROUP_OWNER_HEARTBEAT_MS = parseEnvInt(
206
- process.env.GROUP_OWNER_HEARTBEAT_RUNTIME_MS,
207
- Math.max(1_000, Math.min(GROUP_OWNER_LEASE_MS - 500, Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerHeartbeatMs) || 30_000)),
208
- 1_000,
209
- 5 * 60 * 1000,
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
- ctx.isCommandMessage = ctx.hasCommandPrefix && ctx.isNotifyUpsert;
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 (ctx.hasCommandPrefix && !ctx.isNotifyUpsert) {
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 = () => (ctx, processingResult = '', metadataPatch = null) => {
43
- if (processingResult) {
44
- ctx.analysisPayload.processingResult = processingResult;
45
- }
46
- if (metadataPatch) {
47
- ctx.analysisPayload.metadata = {
48
- ...(ctx.analysisPayload.metadata || {}),
49
- ...(metadataPatch || {}),
50
- };
51
- }
52
- ctx.pipelineStopped = true;
53
- return { stop: true };
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;