@omnizap-system/omnizap 2.6.1 → 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 (172) hide show
  1. package/.env.example +78 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +6 -0
  6. package/app/configParts/adminIdentity.js +36 -7
  7. package/app/configParts/baileysConfig.js +343 -56
  8. package/app/configParts/groupUtils.js +226 -0
  9. package/app/configParts/loggerConfig.js +185 -0
  10. package/app/configParts/messagePersistenceService.js +307 -5
  11. package/app/configParts/sessionConfig.js +242 -0
  12. package/app/connection/baileysCompatibility.test.js +10 -1
  13. package/app/connection/baileysDbAuthState.js +205 -9
  14. package/app/connection/baileysLibsignalPatch.js +210 -0
  15. package/app/connection/groupOwnerWriteStateResolver.js +141 -0
  16. package/app/connection/socketController.js +694 -123
  17. package/app/connection/socketController.multiSession.test.js +128 -0
  18. package/app/controllers/messageController.js +1 -1
  19. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  20. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  21. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  22. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
  23. package/app/controllers/messageProcessingPipeline.js +90 -9
  24. package/app/controllers/messageProcessingPipeline.test.js +202 -0
  25. package/app/modules/adminModule/AGENT.md +1 -1
  26. package/app/modules/adminModule/commandConfig.json +3318 -1347
  27. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  28. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  29. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  30. package/app/modules/aiModule/AGENT.md +47 -30
  31. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  32. package/app/modules/aiModule/catCommand.js +132 -25
  33. package/app/modules/aiModule/commandConfig.json +114 -28
  34. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  35. package/app/modules/gameModule/AGENT.md +1 -1
  36. package/app/modules/gameModule/commandConfig.json +29 -0
  37. package/app/modules/menuModule/AGENT.md +1 -1
  38. package/app/modules/menuModule/commandConfig.json +45 -10
  39. package/app/modules/menuModule/menuCatalogService.js +190 -0
  40. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  41. package/app/modules/menuModule/menuDynamicService.js +511 -0
  42. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  43. package/app/modules/menuModule/menus.js +36 -5
  44. package/app/modules/playModule/AGENT.md +10 -5
  45. package/app/modules/playModule/commandConfig.json +74 -16
  46. package/app/modules/playModule/playCommandConstants.js +13 -7
  47. package/app/modules/playModule/playCommandCore.js +4 -6
  48. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  49. package/app/modules/playModule/playConfigRuntime.js +5 -6
  50. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  51. package/app/modules/quoteModule/AGENT.md +1 -1
  52. package/app/modules/quoteModule/commandConfig.json +29 -0
  53. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  54. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  55. package/app/modules/statsModule/AGENT.md +1 -1
  56. package/app/modules/statsModule/commandConfig.json +58 -0
  57. package/app/modules/stickerModule/AGENT.md +1 -1
  58. package/app/modules/stickerModule/commandConfig.json +145 -0
  59. package/app/modules/stickerPackModule/AGENT.md +1 -1
  60. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  61. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  62. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  63. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  64. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  65. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  66. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  67. package/app/modules/tiktokModule/AGENT.md +1 -1
  68. package/app/modules/tiktokModule/commandConfig.json +29 -0
  69. package/app/modules/userModule/AGENT.md +1 -1
  70. package/app/modules/userModule/commandConfig.json +29 -0
  71. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  72. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  73. package/app/observability/metrics.js +136 -0
  74. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  75. package/app/services/ai/geminiService.js +131 -7
  76. package/app/services/ai/geminiService.test.js +59 -2
  77. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  78. package/app/services/group/groupMetadataService.js +24 -1
  79. package/app/services/infra/dbWriteQueue.js +51 -21
  80. package/app/services/messaging/newsBroadcastService.js +843 -27
  81. package/app/services/multiSession/assignmentBalancerService.js +452 -0
  82. package/app/services/multiSession/groupOwnershipRepository.js +346 -0
  83. package/app/services/multiSession/groupOwnershipService.js +809 -0
  84. package/app/services/multiSession/groupOwnershipService.test.js +317 -0
  85. package/app/services/multiSession/sessionRegistryService.js +239 -0
  86. package/app/store/aiPromptStore.js +36 -19
  87. package/app/store/groupConfigStore.js +41 -5
  88. package/app/store/premiumUserStore.js +21 -7
  89. package/app/utils/antiLink/antiLinkModule.js +391 -25
  90. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  91. package/database/index.js +6 -0
  92. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  93. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  94. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  95. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  96. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  97. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  98. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  99. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  100. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  101. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  102. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  103. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  104. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  105. package/database/schema.sql +102 -1
  106. package/docker-compose.yml +4 -1
  107. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  108. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  109. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  110. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  111. package/docs/security/omnizap-static-security-headers.conf +25 -0
  112. package/ecosystem.prod.config.cjs +31 -11
  113. package/index.js +52 -18
  114. package/observability/alert-rules.yml +20 -0
  115. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  116. package/observability/mysql-setup.sql +4 -4
  117. package/observability/system-admin-observability.md +26 -0
  118. package/package.json +14 -6
  119. package/public/comandos/commands-catalog.json +2253 -78
  120. package/public/css/payments-react.css +478 -0
  121. package/public/js/apps/commandsReactApp.js +267 -87
  122. package/public/js/apps/createPackApp.js +3 -3
  123. package/public/js/apps/homeReactApp.js +2 -2
  124. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  125. package/public/js/apps/paymentsReactApp.js +399 -0
  126. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  127. package/public/js/apps/stickersApp.js +255 -103
  128. package/public/js/apps/termsReactApp.js +57 -8
  129. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  130. package/public/js/apps/userReactApp.js +96 -47
  131. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  132. package/public/pages/pagamentos-cancelado.html +21 -0
  133. package/public/pages/pagamentos-sucesso.html +21 -0
  134. package/public/pages/pagamentos.html +30 -0
  135. package/public/pages/politica-de-privacidade.html +1 -1
  136. package/public/pages/stickers.html +5 -5
  137. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  138. package/public/pages/termos-de-uso.html +1 -1
  139. package/public/pages/user-password-reset.html +3 -4
  140. package/public/pages/user-systemadm.html +8 -462
  141. package/public/pages/user.html +1 -1
  142. package/scripts/clear-whatsapp-session.sh +123 -0
  143. package/scripts/core-ai-mode.mjs +163 -0
  144. package/scripts/deploy.sh +13 -0
  145. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  146. package/scripts/generate-commands-catalog.mjs +155 -0
  147. package/scripts/new-whatsapp-session.sh +564 -0
  148. package/scripts/security-web-surface-check.mjs +218 -0
  149. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  150. package/server/controllers/admin/systemAdminController.js +254 -0
  151. package/server/controllers/payments/paymentsController.js +731 -0
  152. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  153. package/server/controllers/system/contactController.js +9 -17
  154. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  155. package/server/controllers/system/systemController.js +228 -1
  156. package/server/controllers/userController.js +6 -0
  157. package/server/email/emailAutomationRuntime.js +36 -1
  158. package/server/email/emailAutomationService.js +42 -1
  159. package/server/email/emailTemplateService.js +140 -33
  160. package/server/http/httpRequestUtils.js +18 -14
  161. package/server/http/httpServer.js +8 -4
  162. package/server/middleware/securityHeaders.js +35 -3
  163. package/server/routes/admin/systemAdminRouter.js +6 -0
  164. package/server/routes/indexRouter.js +50 -6
  165. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  166. package/server/routes/payments/paymentsRouter.js +47 -0
  167. package/server/routes/static/staticPageRouter.js +30 -1
  168. package/server/utils/publicContact.js +31 -0
  169. package/utils/whatsapp/contactEnv.js +39 -0
  170. package/vite.config.mjs +5 -1
  171. package/app/modules/playModule/local/installYtDlp.js +0 -25
  172. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -1,21 +1,196 @@
1
1
  import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
2
  import { baileysConnectionLogger as logger } from './loggerConfig.js';
3
3
  import { queueMessageInsert } from '../services/infra/dbWriteQueue.js';
4
- import { parseEnvBool, parseEnvInt, normalizeJid, isGroupJid, isStatusJid, isBroadcastJid, isNewsletterJid, normalizeWAPresence } from './baileysConfig.js';
4
+ import { parseEnvBool, parseEnvInt, normalizeJid, isGroupJid, isStatusJid, isBroadcastJid, isNewsletterJid, normalizeWAPresence, isLidJid, isWhatsAppJid, normalizePnToJid, resolveUserId } from './baileysConfig.js';
5
+ import { getOwner as getGroupOwner, tryAcquire as tryAcquireGroupOwner } from '../services/multiSession/groupOwnershipService.js';
5
6
 
7
+ /**
8
+ * Número máximo de tentativas de envio.
9
+ * @type {number}
10
+ */
6
11
  const BAILEYS_SEND_RETRY_ATTEMPTS = parseEnvInt(process.env.BAILEYS_SEND_RETRY_ATTEMPTS, 2, 1, 5);
12
+ /**
13
+ * Atraso base (ms) para backoff exponencial entre retries.
14
+ * @type {number}
15
+ */
7
16
  const BAILEYS_SEND_RETRY_BASE_DELAY_MS = parseEnvInt(process.env.BAILEYS_SEND_RETRY_BASE_DELAY_MS, 600, 100, 10_000);
17
+ /**
18
+ * Timeout de upload de mídia repassado ao Baileys.
19
+ * @type {number}
20
+ */
8
21
  const BAILEYS_SEND_MEDIA_UPLOAD_TIMEOUT_MS = parseEnvInt(process.env.BAILEYS_SEND_MEDIA_UPLOAD_TIMEOUT_MS, 0, 0, 120_000);
22
+ /**
23
+ * Habilita presença automática durante replies.
24
+ * @type {boolean}
25
+ */
9
26
  const BAILEYS_REPLY_PRESENCE_ENABLED = parseEnvBool(process.env.BAILEYS_REPLY_PRESENCE_ENABLED, true);
27
+ /**
28
+ * Define se deve assinar presença antes de enviar update.
29
+ * @type {boolean}
30
+ */
10
31
  const BAILEYS_REPLY_PRESENCE_SUBSCRIBE = parseEnvBool(process.env.BAILEYS_REPLY_PRESENCE_SUBSCRIBE, true);
32
+ /**
33
+ * Delay entre presença "before" e envio (ms).
34
+ * @type {number}
35
+ */
11
36
  const BAILEYS_REPLY_PRESENCE_DELAY_MS = parseEnvInt(process.env.BAILEYS_REPLY_PRESENCE_DELAY_MS, 280, 0, 3_000);
37
+ /**
38
+ * Presença enviada antes do envio.
39
+ * @type {import('@whiskeysockets/baileys').WAPresence}
40
+ */
12
41
  const BAILEYS_REPLY_PRESENCE_BEFORE = normalizeWAPresence(process.env.BAILEYS_REPLY_PRESENCE_BEFORE, 'composing');
42
+ /**
43
+ * Presença enviada após o envio.
44
+ * @type {import('@whiskeysockets/baileys').WAPresence}
45
+ */
13
46
  const BAILEYS_REPLY_PRESENCE_AFTER = normalizeWAPresence(process.env.BAILEYS_REPLY_PRESENCE_AFTER, 'paused');
47
+ /**
48
+ * Prefere enviar para PN quando o destino original for LID.
49
+ * @type {boolean}
50
+ */
51
+ const BAILEYS_SEND_PREFER_PN_FOR_LID = parseEnvBool(process.env.BAILEYS_SEND_PREFER_PN_FOR_LID, true);
52
+ /**
53
+ * TTL do cache de permissão de escrita em grupo (ms).
54
+ * @type {number}
55
+ */
56
+ const GROUP_WRITE_PERMISSION_CACHE_TTL_MS = parseEnvInt(process.env.GROUP_OWNER_WRITE_CACHE_TTL_MS, 8_000, 1_000, 60_000);
14
57
 
58
+ /**
59
+ * Verifica se o valor é um objeto plano.
60
+ * @param {unknown} value
61
+ * @returns {boolean}
62
+ */
15
63
  const isPlainObject = (value) => Object.prototype.toString.call(value) === '[object Object]';
16
64
 
65
+ /**
66
+ * Chaves primárias conhecidas de `AnyMessageContent`.
67
+ * @type {Set<string>}
68
+ */
17
69
  const ANY_MESSAGE_CONTENT_PRIMARY_KEYS = new Set(['text', 'image', 'video', 'audio', 'sticker', 'stickerPack', 'stickerPackMessage', 'document', 'event', 'poll', 'contacts', 'location', 'react', 'buttonReply', 'groupInvite', 'listReply', 'pin', 'product', 'sharePhoneNumber', 'requestPhoneNumber', 'forward', 'delete', 'disappearingMessagesInChat', 'limitSharing']);
70
+ /**
71
+ * Conteúdos que não disparam presença de resposta.
72
+ * @type {Set<string>}
73
+ */
18
74
  const PRESENCE_NON_REPLY_CONTENT_KEYS = new Set(['react', 'delete', 'pin', 'disappearingMessagesInChat']);
75
+ /**
76
+ * Cache local de permissão de escrita por `sessionId+groupJid`.
77
+ * @type {Map<string, {allowed: boolean, ownerSessionId: string | null, expiresAtMs: number}>}
78
+ */
79
+ const groupWritePermissionCache = new Map();
80
+
81
+ /**
82
+ * Normaliza um ID de sessão.
83
+ * @param {unknown} value
84
+ * @returns {string|null}
85
+ */
86
+ const normalizeSessionId = (value) => {
87
+ const normalized = String(value || '').trim();
88
+ return normalized || null;
89
+ };
90
+
91
+ /**
92
+ * Recupera permissão de escrita de grupo no cache.
93
+ * @param {string} groupJid
94
+ * @param {string} sessionId
95
+ * @returns {{allowed: boolean, ownerSessionId: string | null, expiresAtMs: number} | null}
96
+ */
97
+ const getCachedGroupWritePermission = (groupJid, sessionId) => {
98
+ const key = `${sessionId}:${groupJid}`;
99
+ const cached = groupWritePermissionCache.get(key);
100
+ if (!cached) return null;
101
+ if (cached.expiresAtMs <= __timeNowMs()) {
102
+ groupWritePermissionCache.delete(key);
103
+ return null;
104
+ }
105
+ return cached;
106
+ };
107
+
108
+ /**
109
+ * Salva permissão de escrita de grupo no cache.
110
+ * @param {string} groupJid
111
+ * @param {string} sessionId
112
+ * @param {boolean} allowed
113
+ * @param {string|null} [ownerSessionId=null]
114
+ * @returns {void}
115
+ */
116
+ const setCachedGroupWritePermission = (groupJid, sessionId, allowed, ownerSessionId = null) => {
117
+ const key = `${sessionId}:${groupJid}`;
118
+ groupWritePermissionCache.set(key, {
119
+ allowed: Boolean(allowed),
120
+ ownerSessionId: normalizeSessionId(ownerSessionId),
121
+ expiresAtMs: __timeNowMs() + GROUP_WRITE_PERMISSION_CACHE_TTL_MS,
122
+ });
123
+ };
124
+
125
+ /**
126
+ * Resolve se a sessão atual pode escrever em um grupo.
127
+ * @param {string} groupJid
128
+ * @param {string|null} sessionId
129
+ * @returns {Promise<{allowed: boolean, ownerSessionId: string | null, reason: string}>}
130
+ */
131
+ const resolveGroupWritePermission = async (groupJid, sessionId) => {
132
+ if (!isGroupJid(groupJid) || !sessionId) {
133
+ return {
134
+ allowed: true,
135
+ ownerSessionId: null,
136
+ reason: 'not_group_or_missing_session',
137
+ };
138
+ }
139
+
140
+ const cached = getCachedGroupWritePermission(groupJid, sessionId);
141
+ if (cached) {
142
+ return {
143
+ allowed: cached.allowed,
144
+ ownerSessionId: cached.ownerSessionId,
145
+ reason: 'cache_hit',
146
+ };
147
+ }
148
+
149
+ try {
150
+ const ownerState = await getGroupOwner(groupJid);
151
+ let ownerSessionId = normalizeSessionId(ownerState?.ownerSessionId);
152
+ let allowed = false;
153
+ let reason = 'owned_by_other';
154
+
155
+ if (!ownerSessionId) {
156
+ const claimOutcome = await tryAcquireGroupOwner({
157
+ groupJid,
158
+ sessionId,
159
+ reason: 'send_store_claim',
160
+ changedBy: sessionId,
161
+ metadata: {
162
+ source: 'message_persistence_service',
163
+ gate: 'group_write',
164
+ },
165
+ });
166
+ ownerSessionId = normalizeSessionId(claimOutcome?.owner?.ownerSessionId);
167
+ allowed = Boolean(claimOutcome?.acquired && ownerSessionId === sessionId);
168
+ reason = claimOutcome?.reason || 'claim_attempt';
169
+ } else {
170
+ allowed = ownerSessionId === sessionId;
171
+ reason = allowed ? 'owner_match' : 'owned_by_other';
172
+ }
173
+
174
+ setCachedGroupWritePermission(groupJid, sessionId, allowed, ownerSessionId);
175
+ return {
176
+ allowed,
177
+ ownerSessionId,
178
+ reason,
179
+ };
180
+ } catch (error) {
181
+ logger.warn('Falha ao validar ownership para persistência de saída em grupo.', {
182
+ action: 'group_write_permission_resolution_failed',
183
+ groupJid,
184
+ sessionId,
185
+ error: error?.message,
186
+ });
187
+ return {
188
+ allowed: false,
189
+ ownerSessionId: null,
190
+ reason: 'resolution_failed',
191
+ };
192
+ }
193
+ };
19
194
 
20
195
  /**
21
196
  * Verifica se o payload se parece com AnyMessageContent do Baileys.
@@ -62,6 +237,8 @@ const normalizeSendOptions = (options) => {
62
237
  * @param {unknown} options
63
238
  * @returns {{
64
239
  * sendOptions: import('@whiskeysockets/baileys').MiscMessageGenerationOptions|undefined,
240
+ * sessionId: string | null,
241
+ * allowGroupWrite: boolean | undefined,
65
242
  * skipPresenceUpdate: boolean,
66
243
  * presenceBefore: import('@whiskeysockets/baileys').WAPresence,
67
244
  * presenceAfter: import('@whiskeysockets/baileys').WAPresence,
@@ -73,6 +250,8 @@ const resolveRuntimeSendOptions = (options) => {
73
250
  if (!isPlainObject(options)) {
74
251
  return {
75
252
  sendOptions: undefined,
253
+ sessionId: null,
254
+ allowGroupWrite: undefined,
76
255
  skipPresenceUpdate: false,
77
256
  presenceBefore: BAILEYS_REPLY_PRESENCE_BEFORE,
78
257
  presenceAfter: BAILEYS_REPLY_PRESENCE_AFTER,
@@ -81,10 +260,12 @@ const resolveRuntimeSendOptions = (options) => {
81
260
  };
82
261
  }
83
262
 
84
- const { skipPresenceUpdate, presenceBefore, presenceAfter, presenceDelayMs, presenceSubscribe, ...sendOptions } = options;
263
+ const { skipPresenceUpdate, presenceBefore, presenceAfter, presenceDelayMs, presenceSubscribe, sessionId, allowGroupWrite, ...sendOptions } = options;
85
264
  const normalizedDelay = parseEnvInt(presenceDelayMs, BAILEYS_REPLY_PRESENCE_DELAY_MS, 0, 3_000);
86
265
  return {
87
266
  sendOptions: Object.keys(sendOptions).length > 0 ? sendOptions : undefined,
267
+ sessionId: normalizeSessionId(sessionId),
268
+ allowGroupWrite: typeof allowGroupWrite === 'boolean' ? allowGroupWrite : undefined,
88
269
  skipPresenceUpdate: Boolean(skipPresenceUpdate),
89
270
  presenceBefore: normalizeWAPresence(presenceBefore, BAILEYS_REPLY_PRESENCE_BEFORE),
90
271
  presenceAfter: normalizeWAPresence(presenceAfter, BAILEYS_REPLY_PRESENCE_AFTER),
@@ -127,6 +308,60 @@ const shouldSendReplyPresence = (jid, content, runtimeOptions) => {
127
308
  return true;
128
309
  };
129
310
 
311
+ /**
312
+ * Verifica se o JID é de usuário direto (não grupo/broadcast/status/newsletter).
313
+ * @param {string} jid
314
+ * @returns {boolean}
315
+ */
316
+ const isDirectUserJid = (jid) => {
317
+ if (!jid) return false;
318
+ if (isGroupJid(jid)) return false;
319
+ if (isStatusJid(jid)) return false;
320
+ if (isBroadcastJid(jid)) return false;
321
+ if (isNewsletterJid(jid)) return false;
322
+ return true;
323
+ };
324
+
325
+ /**
326
+ * Resolve JID preferencial de envio, convertendo LID para PN quando possível.
327
+ * @param {string} normalizedJid
328
+ * @returns {Promise<string>}
329
+ */
330
+ const resolvePreferredSendJid = async (normalizedJid) => {
331
+ if (!normalizedJid) return normalizedJid;
332
+ if (!BAILEYS_SEND_PREFER_PN_FOR_LID) return normalizedJid;
333
+ if (!isDirectUserJid(normalizedJid)) return normalizedJid;
334
+ if (!isLidJid(normalizedJid)) return normalizedJid;
335
+
336
+ try {
337
+ const resolvedIdentity = await resolveUserId({
338
+ lid: normalizedJid,
339
+ jid: normalizedJid,
340
+ });
341
+ const normalizedResolved = normalizeJid(String(resolvedIdentity || '').trim());
342
+ const candidatePnJid = normalizePnToJid(normalizedResolved || String(resolvedIdentity || '').trim());
343
+ if (candidatePnJid && isWhatsAppJid(candidatePnJid)) {
344
+ return candidatePnJid;
345
+ }
346
+ } catch (error) {
347
+ logger.debug('Falha ao resolver PN para envio com destino LID. Mantendo destino original.', {
348
+ action: 'resolve_preferred_send_jid_failed',
349
+ jid: normalizedJid,
350
+ error: error?.message,
351
+ });
352
+ }
353
+
354
+ return normalizedJid;
355
+ };
356
+
357
+ /**
358
+ * Envia presença no Baileys sem interromper o fluxo principal em caso de erro.
359
+ * @param {import('@whiskeysockets/baileys').WASocket} sock
360
+ * @param {import('@whiskeysockets/baileys').WAPresence} type
361
+ * @param {string} jid
362
+ * @param {boolean} [subscribeFirst=false]
363
+ * @returns {Promise<void>}
364
+ */
130
365
  const sendPresenceSilently = async (sock, type, jid, subscribeFirst = false) => {
131
366
  if (!sock || typeof sock.sendPresenceUpdate !== 'function') return;
132
367
  try {
@@ -163,9 +398,11 @@ const resolveMessageTimestampMs = (msg) => {
163
398
  * Normaliza uma mensagem do Baileys para o formato persistido no banco.
164
399
  * @param {import('@whiskeysockets/baileys').WAMessage} msg - Mensagem recebida/enviada.
165
400
  * @param {string} [senderId] - ID do remetente (opcional).
401
+ * @param {string|null} [sessionId] - Sessão lógica para persistência.
166
402
  * @returns {Object} Objeto com dados prontos para persistencia.
167
403
  */
168
- export const buildMessageData = (msg, senderId) => ({
404
+ export const buildMessageData = (msg, senderId, sessionId = null) => ({
405
+ session_id: normalizeSessionId(sessionId),
169
406
  message_id: msg?.key?.id,
170
407
  chat_id: msg?.key?.remoteJid,
171
408
  sender_id: senderId || msg?.key?.participant || msg?.key?.remoteJid,
@@ -174,8 +411,18 @@ export const buildMessageData = (msg, senderId) => ({
174
411
  timestamp: new Date(resolveMessageTimestampMs(msg)),
175
412
  });
176
413
 
414
+ /**
415
+ * Atrasa execução por `ms`.
416
+ * @param {number} ms
417
+ * @returns {Promise<void>}
418
+ */
177
419
  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
178
420
 
421
+ /**
422
+ * Detecta se um erro de envio é potencialmente transitório.
423
+ * @param {any} error
424
+ * @returns {boolean}
425
+ */
179
426
  const isTransientSendError = (error) => {
180
427
  const statusCode = Number(error?.output?.statusCode || error?.statusCode || 0);
181
428
  if ([408, 409, 425, 429, 500, 502, 503, 504].includes(statusCode)) return true;
@@ -192,6 +439,11 @@ const isTransientSendError = (error) => {
192
439
  return transientFragments.some((fragment) => rawMessage.includes(fragment));
193
440
  };
194
441
 
442
+ /**
443
+ * Indica se o erro sugere refresh explícito de media connection.
444
+ * @param {any} error
445
+ * @returns {boolean}
446
+ */
195
447
  const shouldRefreshMediaConnection = (error) => {
196
448
  const rawMessage = `${error?.message || ''} ${error?.data?.message || ''}`.toLowerCase();
197
449
  return rawMessage.includes('media') || rawMessage.includes('directpath') || rawMessage.includes('upload');
@@ -225,8 +477,52 @@ export async function sendAndStore(sock, jid, content, options) {
225
477
  throw new TypeError(`Payload de mensagem inválido. Chaves recebidas: ${payloadKeys.join(', ') || 'nenhuma'}`);
226
478
  }
227
479
 
228
- const normalizedJid = normalizeJid(jid) || String(jid).trim();
480
+ const normalizedInputJid = normalizeJid(jid) || String(jid).trim();
229
481
  const runtimeOptions = resolveRuntimeSendOptions(options);
482
+ const runtimeSessionId = runtimeOptions.sessionId || normalizeSessionId(sock?.__omnizapSessionId);
483
+ let resolvedGroupWritePermission = null;
484
+
485
+ if (isGroupJid(normalizedInputJid)) {
486
+ if (runtimeOptions.allowGroupWrite === false) {
487
+ logger.debug('Envio para grupo ignorado por bloqueio explícito de escrita.', {
488
+ action: 'send_group_blocked_explicit',
489
+ groupJid: normalizedInputJid,
490
+ sessionId: runtimeSessionId,
491
+ });
492
+ return undefined;
493
+ }
494
+
495
+ if (runtimeOptions.allowGroupWrite === true) {
496
+ resolvedGroupWritePermission = {
497
+ allowed: true,
498
+ ownerSessionId: runtimeSessionId,
499
+ reason: 'explicit_allow',
500
+ };
501
+ } else {
502
+ resolvedGroupWritePermission = await resolveGroupWritePermission(normalizedInputJid, runtimeSessionId);
503
+ if (!resolvedGroupWritePermission.allowed) {
504
+ logger.info('Envio para grupo bloqueado por sessão não-owner.', {
505
+ action: 'send_group_blocked_non_owner',
506
+ groupJid: normalizedInputJid,
507
+ sessionId: runtimeSessionId,
508
+ ownerSessionId: resolvedGroupWritePermission.ownerSessionId,
509
+ reason: resolvedGroupWritePermission.reason,
510
+ });
511
+ return undefined;
512
+ }
513
+ }
514
+ }
515
+
516
+ const normalizedJid = await resolvePreferredSendJid(normalizedInputJid);
517
+ if (normalizedJid !== normalizedInputJid) {
518
+ logger.debug('Destino LID convertido para PN antes do envio.', {
519
+ action: 'send_target_lid_to_pn',
520
+ from: normalizedInputJid,
521
+ to: normalizedJid,
522
+ sessionId: runtimeSessionId,
523
+ });
524
+ }
525
+
230
526
  const normalizedOptions = normalizeSendOptions(runtimeOptions.sendOptions);
231
527
  const shouldSendPresence = shouldSendReplyPresence(normalizedJid, content, runtimeOptions);
232
528
 
@@ -292,7 +588,13 @@ export async function sendAndStore(sock, jid, content, options) {
292
588
  const senderId = sock?.user?.id || sent?.key?.participant;
293
589
  if (sent?.key?.id) {
294
590
  try {
295
- queueMessageInsert(buildMessageData(sent, senderId));
591
+ const messageData = buildMessageData(sent, senderId, runtimeSessionId);
592
+ const targetGroupJid = normalizeJid(messageData.chat_id || normalizedInputJid);
593
+ if (isGroupJid(targetGroupJid)) {
594
+ const allowGroupWrite = runtimeOptions.allowGroupWrite === true || resolvedGroupWritePermission?.allowed === true;
595
+ messageData.allow_group_write = allowGroupWrite;
596
+ }
597
+ queueMessageInsert(messageData);
296
598
  } catch (error) {
297
599
  logger.warn('Falha ao enfileirar mensagem enviada para persistencia.', {
298
600
  error: error.message,
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Sessão padrão usada como fallback.
3
+ * @type {string}
4
+ */
5
+ const DEFAULT_SESSION_ID = 'default';
6
+ /**
7
+ * Tamanho máximo permitido para IDs de sessão.
8
+ * @type {number}
9
+ */
10
+ const SESSION_ID_MAX_LENGTH = 64;
11
+ /**
12
+ * Regex de validação para IDs de sessão.
13
+ * @type {RegExp}
14
+ */
15
+ const SESSION_ID_PATTERN = /^[a-zA-Z0-9:_-]+$/;
16
+ /**
17
+ * Modos de enforcement válidos para ownership de grupo.
18
+ * @type {Set<string>}
19
+ */
20
+ const OWNER_ENFORCEMENT_MODES = new Set(['off', 'shadow', 'enforce']);
21
+
22
+ /**
23
+ * Converte um valor de ambiente para boolean com fallback.
24
+ * @param {unknown} value
25
+ * @param {boolean} fallback
26
+ * @returns {boolean}
27
+ */
28
+ const parseEnvBool = (value, fallback) => {
29
+ if (value === undefined || value === null || value === '') return fallback;
30
+ const normalized = String(value).trim().toLowerCase();
31
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
32
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
33
+ return fallback;
34
+ };
35
+
36
+ /**
37
+ * Converte um valor em inteiro com limites.
38
+ * @param {unknown} value
39
+ * @param {number} fallback
40
+ * @param {number} min
41
+ * @param {number} max
42
+ * @returns {number}
43
+ */
44
+ const parseEnvInt = (value, fallback, min, max) => {
45
+ const parsed = Number(value);
46
+ if (!Number.isFinite(parsed)) return fallback;
47
+ return Math.max(min, Math.min(max, Math.floor(parsed)));
48
+ };
49
+
50
+ /**
51
+ * Faz parse de entradas separadas por vírgula, quebra de linha ou `;`.
52
+ * @param {unknown} value
53
+ * @returns {string[]}
54
+ */
55
+ const parseFlexibleEntries = (value) =>
56
+ String(value || '')
57
+ .split(/[,\n;]+/g)
58
+ .map((entry) => String(entry || '').trim())
59
+ .filter(Boolean);
60
+
61
+ /**
62
+ * Normaliza um ID de sessão.
63
+ * @param {unknown} value
64
+ * @returns {string}
65
+ */
66
+ const normalizeSessionId = (value) => String(value || '').trim();
67
+
68
+ /**
69
+ * Valida formato e tamanho de ID de sessão.
70
+ * @param {unknown} value
71
+ * @returns {boolean}
72
+ */
73
+ const isValidSessionId = (value) => {
74
+ const normalized = normalizeSessionId(value);
75
+ if (!normalized) return false;
76
+ if (normalized.length > SESSION_ID_MAX_LENGTH) return false;
77
+ return SESSION_ID_PATTERN.test(normalized);
78
+ };
79
+
80
+ /**
81
+ * @typedef {{sessionIds: string[], warnings: string[]}} ParsedSessionIds
82
+ */
83
+ /**
84
+ * Interpreta lista de sessões do ambiente e aplica fallback legado.
85
+ * @param {{sessionIdsRaw?: string, legacySessionIdRaw?: string}} [params]
86
+ * @returns {ParsedSessionIds}
87
+ */
88
+ const parseSessionIds = ({ sessionIdsRaw = '', legacySessionIdRaw = '' } = {}) => {
89
+ const warnings = [];
90
+ const validSessionIds = [];
91
+ const seen = new Set();
92
+
93
+ const legacySessionId = normalizeSessionId(legacySessionIdRaw) || DEFAULT_SESSION_ID;
94
+ const requestedSessionIds = parseFlexibleEntries(sessionIdsRaw);
95
+ const sourceSessionIds = requestedSessionIds.length > 0 ? requestedSessionIds : [legacySessionId];
96
+
97
+ for (const candidate of sourceSessionIds) {
98
+ const sessionId = normalizeSessionId(candidate);
99
+ if (!isValidSessionId(sessionId)) {
100
+ warnings.push(`session_id invalido ignorado: "${candidate}"`);
101
+ continue;
102
+ }
103
+ if (seen.has(sessionId)) continue;
104
+ seen.add(sessionId);
105
+ validSessionIds.push(sessionId);
106
+ }
107
+
108
+ if (validSessionIds.length === 0) {
109
+ validSessionIds.push(DEFAULT_SESSION_ID);
110
+ warnings.push(`nenhum session_id valido encontrado; usando fallback "${DEFAULT_SESSION_ID}"`);
111
+ }
112
+
113
+ return {
114
+ sessionIds: validSessionIds,
115
+ warnings,
116
+ };
117
+ };
118
+
119
+ /**
120
+ * Resolve pesos por sessão com validação e defaults.
121
+ * @param {unknown} rawValue
122
+ * @param {string[]} sessionIds
123
+ * @param {string[]} warnings
124
+ * @returns {Record<string, number>}
125
+ */
126
+ const parseSessionWeights = (rawValue, sessionIds, warnings) => {
127
+ const allowedSessions = new Set(sessionIds);
128
+ const weights = {};
129
+ for (const sessionId of sessionIds) {
130
+ weights[sessionId] = 1;
131
+ }
132
+
133
+ const entries = parseFlexibleEntries(rawValue);
134
+ if (entries.length === 0) return weights;
135
+
136
+ for (const entry of entries) {
137
+ const separator = entry.includes('=') ? '=' : entry.includes(':') ? ':' : '';
138
+ if (!separator) {
139
+ warnings.push(`peso de sessao invalido (faltando separador "=" ou ":"): "${entry}"`);
140
+ continue;
141
+ }
142
+
143
+ const [rawSessionId, rawWeight] = entry.split(separator, 2);
144
+ const sessionId = normalizeSessionId(rawSessionId);
145
+ if (!isValidSessionId(sessionId)) {
146
+ warnings.push(`peso ignorado para session_id invalido: "${rawSessionId}"`);
147
+ continue;
148
+ }
149
+ if (!allowedSessions.has(sessionId)) {
150
+ warnings.push(`peso ignorado para session_id nao listado em BAILEYS_SESSION_IDS: "${sessionId}"`);
151
+ continue;
152
+ }
153
+
154
+ const weight = parseEnvInt(rawWeight, Number.NaN, 1, 1000);
155
+ if (!Number.isFinite(weight)) {
156
+ warnings.push(`peso invalido para "${sessionId}": "${rawWeight}"`);
157
+ continue;
158
+ }
159
+ weights[sessionId] = weight;
160
+ }
161
+
162
+ return weights;
163
+ };
164
+
165
+ /**
166
+ * @typedef {{
167
+ * sessionIds: readonly string[],
168
+ * primarySessionId: string,
169
+ * sessionWeights: Readonly<Record<string, number>>,
170
+ * ownerEnforcementMode: 'off'|'shadow'|'enforce',
171
+ * ownerLeaseMs: number,
172
+ * ownerHeartbeatMs: number,
173
+ * balancerEnabled: boolean,
174
+ * warnings: readonly string[]
175
+ * }} MultiSessionRuntimeConfig
176
+ */
177
+ /**
178
+ * Resolve a configuração de runtime para múltiplas sessões.
179
+ * @param {Record<string, any>} [env=process.env]
180
+ * @returns {MultiSessionRuntimeConfig}
181
+ */
182
+ export const resolveMultiSessionRuntimeConfig = (env = process.env) => {
183
+ const warnings = [];
184
+ const legacySessionId = normalizeSessionId(env.BAILEYS_AUTH_SESSION_ID) || DEFAULT_SESSION_ID;
185
+ const { sessionIds, warnings: parseWarnings } = parseSessionIds({
186
+ sessionIdsRaw: env.BAILEYS_SESSION_IDS,
187
+ legacySessionIdRaw: legacySessionId,
188
+ });
189
+ warnings.push(...parseWarnings);
190
+
191
+ const requestedPrimary = normalizeSessionId(env.BAILEYS_PRIMARY_SESSION_ID);
192
+ let primarySessionId = requestedPrimary || sessionIds[0];
193
+
194
+ if (requestedPrimary && !isValidSessionId(requestedPrimary)) {
195
+ warnings.push(`BAILEYS_PRIMARY_SESSION_ID invalido: "${requestedPrimary}"`);
196
+ primarySessionId = sessionIds[0];
197
+ } else if (requestedPrimary && !sessionIds.includes(requestedPrimary)) {
198
+ warnings.push(`BAILEYS_PRIMARY_SESSION_ID fora da lista de sessoes: "${requestedPrimary}"`);
199
+ primarySessionId = sessionIds[0];
200
+ }
201
+
202
+ const ownerEnforcementModeRaw = String(env.GROUP_OWNER_ENFORCEMENT_MODE || 'off')
203
+ .trim()
204
+ .toLowerCase();
205
+ const ownerEnforcementMode = OWNER_ENFORCEMENT_MODES.has(ownerEnforcementModeRaw) ? ownerEnforcementModeRaw : 'off';
206
+ if (!OWNER_ENFORCEMENT_MODES.has(ownerEnforcementModeRaw)) {
207
+ warnings.push(`GROUP_OWNER_ENFORCEMENT_MODE invalido: "${ownerEnforcementModeRaw}"`);
208
+ }
209
+
210
+ const ownerLeaseMs = parseEnvInt(env.GROUP_OWNER_LEASE_MS, 120_000, 5_000, 15 * 60 * 1000);
211
+ let ownerHeartbeatMs = parseEnvInt(env.GROUP_OWNER_HEARTBEAT_MS, 30_000, 1_000, 5 * 60 * 1000);
212
+ if (ownerHeartbeatMs >= ownerLeaseMs) {
213
+ ownerHeartbeatMs = Math.max(1_000, Math.floor(ownerLeaseMs / 2));
214
+ warnings.push(`GROUP_OWNER_HEARTBEAT_MS ajustado automaticamente para ${ownerHeartbeatMs}ms (precisa ser menor que lease)`);
215
+ }
216
+
217
+ const balancerEnabled = parseEnvBool(env.GROUP_BALANCER_ENABLED, false);
218
+ const sessionWeights = parseSessionWeights(env.BAILEYS_SESSION_WEIGHTS, sessionIds, warnings);
219
+
220
+ return Object.freeze({
221
+ sessionIds: Object.freeze([...sessionIds]),
222
+ primarySessionId,
223
+ sessionWeights: Object.freeze({ ...sessionWeights }),
224
+ ownerEnforcementMode,
225
+ ownerLeaseMs,
226
+ ownerHeartbeatMs,
227
+ balancerEnabled,
228
+ warnings: Object.freeze([...warnings]),
229
+ });
230
+ };
231
+
232
+ /**
233
+ * Configuração resolvida uma única vez por processo.
234
+ * @type {MultiSessionRuntimeConfig}
235
+ */
236
+ export const multiSessionRuntimeConfig = resolveMultiSessionRuntimeConfig();
237
+
238
+ /**
239
+ * Retorna a configuração de runtime multi-sessão.
240
+ * @returns {MultiSessionRuntimeConfig}
241
+ */
242
+ export const getMultiSessionRuntimeConfig = () => multiSessionRuntimeConfig;
@@ -6,12 +6,21 @@ import test from 'node:test';
6
6
 
7
7
  import { initAuthCreds, proto } from '@whiskeysockets/baileys';
8
8
 
9
- const PINNED_BAILEYS_REF = 'github:jlucaso1/Baileys#be89465e07afa871cf3f0e19cabfec9780db6be7';
9
+ /**
10
+ * Referência do fork/branch do Baileys validada pelos testes.
11
+ * @type {string}
12
+ */
13
+ const PINNED_BAILEYS_REF = 'github:jlucaso1/Baileys#feat-add-stickerpack-support';
10
14
 
11
15
  const require = createRequire(import.meta.url);
12
16
  const baileysPackageJsonPath = require.resolve('@whiskeysockets/baileys/package.json');
13
17
  const baileysPackageDir = path.dirname(baileysPackageJsonPath);
14
18
 
19
+ /**
20
+ * Lê um arquivo de tipos dentro do pacote instalado do Baileys.
21
+ * @param {string} relativePath
22
+ * @returns {Promise<string>}
23
+ */
15
24
  const readBaileysTypeFile = async (relativePath) => readFile(path.join(baileysPackageDir, relativePath), 'utf8');
16
25
 
17
26
  test('Auth.d.ts expõe AuthenticationState compatível com SocketConfig.auth', async () => {