@omnizap-system/omnizap 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.env.example +58 -13
  2. package/.github/workflows/ci.yml +5 -5
  3. package/.github/workflows/codeql.yml +1 -1
  4. package/.github/workflows/db-migration-check.yml +2 -2
  5. package/.github/workflows/dependency-review.yml +1 -1
  6. package/.github/workflows/deploy.yml +2 -2
  7. package/.github/workflows/release.yml +2 -2
  8. package/.github/workflows/security-attest-provenance.yml +2 -2
  9. package/.github/workflows/security-gitleaks.yml +13 -4
  10. package/.github/workflows/security-runner-hardening.yml +2 -2
  11. package/.github/workflows/security-scorecard.yml +1 -1
  12. package/.github/workflows/security-zap-baseline.yml +1 -1
  13. package/.github/workflows/security-zap-full-scan.yml +2 -1
  14. package/.github/workflows/security-zizmor.yml +1 -1
  15. package/.github/workflows/wiki-sync.yml +1 -1
  16. package/.gitleaksignore +9 -0
  17. package/CODE_OF_CONDUCT.md +2 -2
  18. package/GEMINI.md +64 -0
  19. package/README.md +52 -82
  20. package/SECURITY.md +1 -1
  21. package/app/config/index.js +2 -0
  22. package/app/configParts/adminIdentity.js +5 -5
  23. package/app/configParts/baileysConfig.js +230 -58
  24. package/app/configParts/groupUtils.js +5 -0
  25. package/app/configParts/messagePersistenceService.js +145 -4
  26. package/app/configParts/sessionConfig.js +157 -0
  27. package/app/connection/baileysCompatibility.test.js +1 -1
  28. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  29. package/app/connection/socketController.js +660 -158
  30. package/app/connection/socketController.multiSession.test.js +108 -0
  31. package/app/controllers/messageController.js +1 -1
  32. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  33. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  34. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  35. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  36. package/app/controllers/messageProcessingPipeline.js +93 -13
  37. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  38. package/app/modules/adminModule/AGENT.md +1 -1
  39. package/app/modules/adminModule/commandConfig.json +3318 -1347
  40. package/app/modules/adminModule/groupCommandHandlers.js +858 -15
  41. package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
  42. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  43. package/app/modules/aiModule/AGENT.md +47 -30
  44. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  45. package/app/modules/aiModule/catCommand.js +135 -27
  46. package/app/modules/aiModule/commandConfig.json +114 -28
  47. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  48. package/app/modules/gameModule/AGENT.md +1 -1
  49. package/app/modules/gameModule/commandConfig.json +29 -0
  50. package/app/modules/menuModule/AGENT.md +1 -1
  51. package/app/modules/menuModule/commandConfig.json +45 -10
  52. package/app/modules/menuModule/menuCatalogService.js +190 -0
  53. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  54. package/app/modules/menuModule/menuDynamicService.js +511 -0
  55. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  56. package/app/modules/menuModule/menus.js +36 -5
  57. package/app/modules/playModule/AGENT.md +10 -5
  58. package/app/modules/playModule/commandConfig.json +140 -12
  59. package/app/modules/playModule/playCommand.js +1 -1417
  60. package/app/modules/playModule/playCommandConstants.js +80 -0
  61. package/app/modules/playModule/playCommandCore.js +361 -0
  62. package/app/modules/playModule/playCommandHandlers.js +41 -0
  63. package/app/modules/playModule/playCommandMediaClient.js +1872 -0
  64. package/app/modules/playModule/playConfigRuntime.js +245 -4
  65. package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
  66. package/app/modules/quoteModule/AGENT.md +1 -1
  67. package/app/modules/quoteModule/commandConfig.json +29 -0
  68. package/app/modules/quoteModule/quoteCommand.js +3 -2
  69. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  70. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  71. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
  72. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
  73. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
  74. package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
  75. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
  76. package/app/modules/statsModule/AGENT.md +1 -1
  77. package/app/modules/statsModule/commandConfig.json +58 -0
  78. package/app/modules/statsModule/rankingCommon.js +5 -4
  79. package/app/modules/stickerModule/AGENT.md +1 -1
  80. package/app/modules/stickerModule/addStickerMetadata.js +4 -3
  81. package/app/modules/stickerModule/commandConfig.json +145 -0
  82. package/app/modules/stickerModule/stickerCommand.js +1 -1
  83. package/app/modules/stickerPackModule/AGENT.md +1 -1
  84. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  85. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  86. package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
  87. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
  88. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
  89. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
  90. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
  91. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
  92. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
  93. package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
  94. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
  95. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  96. package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
  97. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
  98. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  99. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  100. package/app/modules/systemMetricsModule/pingCommand.js +6 -5
  101. package/app/modules/tiktokModule/AGENT.md +1 -1
  102. package/app/modules/tiktokModule/commandConfig.json +29 -0
  103. package/app/modules/tiktokModule/tiktokCommand.js +2 -1
  104. package/app/modules/userModule/AGENT.md +1 -1
  105. package/app/modules/userModule/commandConfig.json +29 -0
  106. package/app/modules/userModule/userCommand.js +72 -23
  107. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  108. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  109. package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
  110. package/app/observability/metrics.js +136 -0
  111. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  112. package/app/services/ai/conversationRouterService.js +4 -3
  113. package/app/services/ai/geminiService.js +132 -7
  114. package/app/services/ai/geminiService.test.js +59 -2
  115. package/app/services/ai/globalModuleAiHelpService.js +3 -2
  116. package/app/services/ai/messageCommandExecutionService.js +2 -1
  117. package/app/services/ai/moduleAiHelpCoreService.js +45 -14
  118. package/app/services/ai/moduleToolExecutorService.js +3 -2
  119. package/app/services/ai/moduleToolRegistryService.js +2 -1
  120. package/app/services/ai/toolCandidateSelectorService.js +6 -5
  121. package/app/services/auth/googleWebLinkService.js +3 -2
  122. package/app/services/auth/whatsappLoginLinkService.js +3 -2
  123. package/app/services/external/pokeApiService.js +4 -3
  124. package/app/services/group/groupMetadataService.js +24 -1
  125. package/app/services/infra/dbWriteQueue.js +57 -26
  126. package/app/services/infra/featureFlagService.js +2 -1
  127. package/app/services/messaging/captchaService.js +3 -2
  128. package/app/services/messaging/newsBroadcastService.js +846 -29
  129. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  130. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  131. package/app/services/multiSession/groupOwnershipService.js +890 -0
  132. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  133. package/app/services/multiSession/sessionRegistryService.js +293 -0
  134. package/app/services/sticker/stickerFocusService.js +11 -10
  135. package/app/store/aiPromptStore.js +36 -19
  136. package/app/store/conversationSessionStore.js +7 -6
  137. package/app/store/groupConfigStore.js +41 -5
  138. package/app/store/premiumUserStore.js +21 -7
  139. package/app/utils/antiLink/antiLinkModule.js +352 -16
  140. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  141. package/app/workers/aiLearningWorker.js +6 -5
  142. package/app/workers/commandConfigEnrichmentWorker.js +4 -3
  143. package/database/index.js +14 -8
  144. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  145. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  146. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  147. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  148. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  149. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  150. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  151. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  152. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  153. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  154. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  155. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  156. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  157. package/database/schema.sql +102 -1
  158. package/docker-compose.yml +4 -1
  159. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  160. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
  161. package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
  162. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  163. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
  164. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  165. package/docs/security/omnizap-static-security-headers.conf +25 -0
  166. package/docs/wiki/Home.md +1 -1
  167. package/ecosystem.prod.config.cjs +32 -12
  168. package/index.js +57 -23
  169. package/observability/alert-rules.yml +20 -0
  170. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  171. package/observability/mysql-setup.sql +4 -4
  172. package/observability/system-admin-observability.md +26 -0
  173. package/package.json +20 -6
  174. package/public/apple-touch-icon.png +0 -0
  175. package/public/comandos/commands-catalog.json +2853 -3326
  176. package/public/favicon-16x16.png +0 -0
  177. package/public/favicon-32x32.png +0 -0
  178. package/public/favicon.ico +0 -0
  179. package/public/js/apps/apiDocsApp.js +3 -2
  180. package/public/js/apps/commandsReactApp.js +280 -99
  181. package/public/js/apps/createPackApp.js +11 -10
  182. package/public/js/apps/homeReactApp.js +181 -130
  183. package/public/js/apps/loginReactApp.js +1 -1
  184. package/public/js/apps/stickersApp.js +263 -110
  185. package/public/js/apps/termsReactApp.js +73 -24
  186. package/public/js/apps/userApp.js +4 -3
  187. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  188. package/public/js/apps/userReactApp.js +355 -280
  189. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  190. package/public/pages/api-docs.html +1 -1
  191. package/public/pages/aup.html +2 -2
  192. package/public/pages/dpa.html +3 -3
  193. package/public/pages/licenca.html +4 -4
  194. package/public/pages/login.html +1 -1
  195. package/public/pages/notice-and-takedown.html +2 -2
  196. package/public/pages/politica-de-privacidade.html +6 -6
  197. package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
  198. package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
  199. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
  200. package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
  201. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
  202. package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
  203. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
  204. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
  205. package/public/pages/stickers-admin.html +1 -1
  206. package/public/pages/stickers-create.html +1 -1
  207. package/public/pages/stickers.html +6 -6
  208. package/public/pages/suboperadores.html +2 -2
  209. package/public/pages/termos-de-uso-texto-integral.html +6 -6
  210. package/public/pages/termos-de-uso.html +3 -3
  211. package/public/pages/user-password-reset.html +4 -5
  212. package/public/pages/user-systemadm.html +9 -463
  213. package/public/pages/user.html +2 -2
  214. package/scripts/clear-whatsapp-session.sh +123 -0
  215. package/scripts/core-ai-mode.mjs +163 -0
  216. package/scripts/deploy.sh +11 -1
  217. package/scripts/email-broadcast-terms-update.mjs +2 -1
  218. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  219. package/scripts/generate-commands-catalog.mjs +166 -2
  220. package/scripts/generate-module-agents.mjs +2 -1
  221. package/scripts/generate-seo-satellite-pages.mjs +5 -4
  222. package/scripts/github-deploy-notify.mjs +2 -1
  223. package/scripts/github-release-notify.mjs +25 -10
  224. package/scripts/new-whatsapp-session.sh +317 -0
  225. package/scripts/release.sh +2 -19
  226. package/scripts/security-smoketest.mjs +6 -5
  227. package/scripts/security-web-surface-check.mjs +218 -0
  228. package/scripts/sticker-catalog-loadtest.mjs +5 -4
  229. package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
  230. package/server/auth/jwt/webJwtService.js +1 -1
  231. package/server/auth/stickerCatalogAuthContext.js +2 -1
  232. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
  233. package/server/auth/userPassword/userPasswordAuthService.js +2 -1
  234. package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
  235. package/server/auth/webAccount/webAccountHandlers.js +9 -10
  236. package/server/controllers/admin/adminPanelHandlers.js +267 -16
  237. package/server/controllers/admin/systemAdminController.js +267 -0
  238. package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
  239. package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
  240. package/server/controllers/sticker/stickerCatalogController.js +23 -36
  241. package/server/controllers/system/contactController.js +9 -17
  242. package/server/controllers/system/githubController.js +3 -2
  243. package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
  244. package/server/controllers/system/systemController.js +254 -1
  245. package/server/controllers/system/systemMetricsController.js +2 -1
  246. package/server/controllers/userController.js +6 -0
  247. package/server/email/emailTemplateService.js +5 -3
  248. package/server/http/httpServer.js +11 -6
  249. package/server/middleware/rateLimit.js +2 -1
  250. package/server/middleware/securityHeaders.js +20 -1
  251. package/server/routes/admin/systemAdminRouter.js +6 -0
  252. package/server/routes/indexRouter.js +30 -6
  253. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  254. package/server/routes/static/staticPageRouter.js +27 -1
  255. package/server/utils/publicContact.js +31 -0
  256. package/utils/time/timeModule.js +135 -0
  257. package/utils/time/timeModule.test.js +65 -0
  258. package/utils/whatsapp/contactEnv.js +39 -0
  259. package/vite.config.mjs +7 -1
  260. package/public/assets/images/brand-icon-192.png +0 -0
  261. package/scripts/sync-readme-snapshot.mjs +0 -133
@@ -1,6 +1,8 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { baileysConnectionLogger as logger } from './loggerConfig.js';
2
3
  import { queueMessageInsert } from '../services/infra/dbWriteQueue.js';
3
4
  import { parseEnvBool, parseEnvInt, normalizeJid, isGroupJid, isStatusJid, isBroadcastJid, isNewsletterJid, normalizeWAPresence } from './baileysConfig.js';
5
+ import { getOwner as getGroupOwner, tryAcquire as tryAcquireGroupOwner } from '../services/multiSession/groupOwnershipService.js';
4
6
 
5
7
  const BAILEYS_SEND_RETRY_ATTEMPTS = parseEnvInt(process.env.BAILEYS_SEND_RETRY_ATTEMPTS, 2, 1, 5);
6
8
  const BAILEYS_SEND_RETRY_BASE_DELAY_MS = parseEnvInt(process.env.BAILEYS_SEND_RETRY_BASE_DELAY_MS, 600, 100, 10_000);
@@ -10,11 +12,102 @@ const BAILEYS_REPLY_PRESENCE_SUBSCRIBE = parseEnvBool(process.env.BAILEYS_REPLY_
10
12
  const BAILEYS_REPLY_PRESENCE_DELAY_MS = parseEnvInt(process.env.BAILEYS_REPLY_PRESENCE_DELAY_MS, 280, 0, 3_000);
11
13
  const BAILEYS_REPLY_PRESENCE_BEFORE = normalizeWAPresence(process.env.BAILEYS_REPLY_PRESENCE_BEFORE, 'composing');
12
14
  const BAILEYS_REPLY_PRESENCE_AFTER = normalizeWAPresence(process.env.BAILEYS_REPLY_PRESENCE_AFTER, 'paused');
15
+ const GROUP_WRITE_PERMISSION_CACHE_TTL_MS = parseEnvInt(process.env.GROUP_OWNER_WRITE_CACHE_TTL_MS, 8_000, 1_000, 60_000);
13
16
 
14
17
  const isPlainObject = (value) => Object.prototype.toString.call(value) === '[object Object]';
15
18
 
16
19
  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']);
17
20
  const PRESENCE_NON_REPLY_CONTENT_KEYS = new Set(['react', 'delete', 'pin', 'disappearingMessagesInChat']);
21
+ const groupWritePermissionCache = new Map();
22
+
23
+ const normalizeSessionId = (value) => {
24
+ const normalized = String(value || '').trim();
25
+ return normalized || null;
26
+ };
27
+
28
+ const getCachedGroupWritePermission = (groupJid, sessionId) => {
29
+ const key = `${sessionId}:${groupJid}`;
30
+ const cached = groupWritePermissionCache.get(key);
31
+ if (!cached) return null;
32
+ if (cached.expiresAtMs <= __timeNowMs()) {
33
+ groupWritePermissionCache.delete(key);
34
+ return null;
35
+ }
36
+ return cached;
37
+ };
38
+
39
+ const setCachedGroupWritePermission = (groupJid, sessionId, allowed, ownerSessionId = null) => {
40
+ const key = `${sessionId}:${groupJid}`;
41
+ groupWritePermissionCache.set(key, {
42
+ allowed: Boolean(allowed),
43
+ ownerSessionId: normalizeSessionId(ownerSessionId),
44
+ expiresAtMs: __timeNowMs() + GROUP_WRITE_PERMISSION_CACHE_TTL_MS,
45
+ });
46
+ };
47
+
48
+ const resolveGroupWritePermission = async (groupJid, sessionId) => {
49
+ if (!isGroupJid(groupJid) || !sessionId) {
50
+ return {
51
+ allowed: true,
52
+ ownerSessionId: null,
53
+ reason: 'not_group_or_missing_session',
54
+ };
55
+ }
56
+
57
+ const cached = getCachedGroupWritePermission(groupJid, sessionId);
58
+ if (cached) {
59
+ return {
60
+ allowed: cached.allowed,
61
+ ownerSessionId: cached.ownerSessionId,
62
+ reason: 'cache_hit',
63
+ };
64
+ }
65
+
66
+ try {
67
+ const ownerState = await getGroupOwner(groupJid);
68
+ let ownerSessionId = normalizeSessionId(ownerState?.ownerSessionId);
69
+ let allowed = false;
70
+ let reason = 'owned_by_other';
71
+
72
+ if (!ownerSessionId) {
73
+ const claimOutcome = await tryAcquireGroupOwner({
74
+ groupJid,
75
+ sessionId,
76
+ reason: 'send_store_claim',
77
+ changedBy: sessionId,
78
+ metadata: {
79
+ source: 'message_persistence_service',
80
+ gate: 'group_write',
81
+ },
82
+ });
83
+ ownerSessionId = normalizeSessionId(claimOutcome?.owner?.ownerSessionId);
84
+ allowed = Boolean(claimOutcome?.acquired && ownerSessionId === sessionId);
85
+ reason = claimOutcome?.reason || 'claim_attempt';
86
+ } else {
87
+ allowed = ownerSessionId === sessionId;
88
+ reason = allowed ? 'owner_match' : 'owned_by_other';
89
+ }
90
+
91
+ setCachedGroupWritePermission(groupJid, sessionId, allowed, ownerSessionId);
92
+ return {
93
+ allowed,
94
+ ownerSessionId,
95
+ reason,
96
+ };
97
+ } catch (error) {
98
+ logger.warn('Falha ao validar ownership para persistência de saída em grupo.', {
99
+ action: 'group_write_permission_resolution_failed',
100
+ groupJid,
101
+ sessionId,
102
+ error: error?.message,
103
+ });
104
+ return {
105
+ allowed: false,
106
+ ownerSessionId: null,
107
+ reason: 'resolution_failed',
108
+ };
109
+ }
110
+ };
18
111
 
19
112
  /**
20
113
  * Verifica se o payload se parece com AnyMessageContent do Baileys.
@@ -61,6 +154,8 @@ const normalizeSendOptions = (options) => {
61
154
  * @param {unknown} options
62
155
  * @returns {{
63
156
  * sendOptions: import('@whiskeysockets/baileys').MiscMessageGenerationOptions|undefined,
157
+ * sessionId: string | null,
158
+ * allowGroupWrite: boolean | undefined,
64
159
  * skipPresenceUpdate: boolean,
65
160
  * presenceBefore: import('@whiskeysockets/baileys').WAPresence,
66
161
  * presenceAfter: import('@whiskeysockets/baileys').WAPresence,
@@ -72,6 +167,8 @@ const resolveRuntimeSendOptions = (options) => {
72
167
  if (!isPlainObject(options)) {
73
168
  return {
74
169
  sendOptions: undefined,
170
+ sessionId: null,
171
+ allowGroupWrite: undefined,
75
172
  skipPresenceUpdate: false,
76
173
  presenceBefore: BAILEYS_REPLY_PRESENCE_BEFORE,
77
174
  presenceAfter: BAILEYS_REPLY_PRESENCE_AFTER,
@@ -80,10 +177,12 @@ const resolveRuntimeSendOptions = (options) => {
80
177
  };
81
178
  }
82
179
 
83
- const { skipPresenceUpdate, presenceBefore, presenceAfter, presenceDelayMs, presenceSubscribe, ...sendOptions } = options;
180
+ const { skipPresenceUpdate, presenceBefore, presenceAfter, presenceDelayMs, presenceSubscribe, sessionId, allowGroupWrite, ...sendOptions } = options;
84
181
  const normalizedDelay = parseEnvInt(presenceDelayMs, BAILEYS_REPLY_PRESENCE_DELAY_MS, 0, 3_000);
85
182
  return {
86
183
  sendOptions: Object.keys(sendOptions).length > 0 ? sendOptions : undefined,
184
+ sessionId: normalizeSessionId(sessionId),
185
+ allowGroupWrite: typeof allowGroupWrite === 'boolean' ? allowGroupWrite : undefined,
87
186
  skipPresenceUpdate: Boolean(skipPresenceUpdate),
88
187
  presenceBefore: normalizeWAPresence(presenceBefore, BAILEYS_REPLY_PRESENCE_BEFORE),
89
188
  presenceAfter: normalizeWAPresence(presenceAfter, BAILEYS_REPLY_PRESENCE_AFTER),
@@ -155,16 +254,18 @@ const resolveMessageTimestampMs = (msg) => {
155
254
  return tsNumber * 1000;
156
255
  }
157
256
  }
158
- return Date.now();
257
+ return __timeNowMs();
159
258
  };
160
259
 
161
260
  /**
162
261
  * Normaliza uma mensagem do Baileys para o formato persistido no banco.
163
262
  * @param {import('@whiskeysockets/baileys').WAMessage} msg - Mensagem recebida/enviada.
164
263
  * @param {string} [senderId] - ID do remetente (opcional).
264
+ * @param {string|null} [sessionId] - Sessão lógica para persistência.
165
265
  * @returns {Object} Objeto com dados prontos para persistencia.
166
266
  */
167
- export const buildMessageData = (msg, senderId) => ({
267
+ export const buildMessageData = (msg, senderId, sessionId = null) => ({
268
+ session_id: normalizeSessionId(sessionId),
168
269
  message_id: msg?.key?.id,
169
270
  chat_id: msg?.key?.remoteJid,
170
271
  sender_id: senderId || msg?.key?.participant || msg?.key?.remoteJid,
@@ -226,6 +327,40 @@ export async function sendAndStore(sock, jid, content, options) {
226
327
 
227
328
  const normalizedJid = normalizeJid(jid) || String(jid).trim();
228
329
  const runtimeOptions = resolveRuntimeSendOptions(options);
330
+ const runtimeSessionId = runtimeOptions.sessionId || normalizeSessionId(sock?.__omnizapSessionId);
331
+ let resolvedGroupWritePermission = null;
332
+
333
+ if (isGroupJid(normalizedJid)) {
334
+ if (runtimeOptions.allowGroupWrite === false) {
335
+ logger.debug('Envio para grupo ignorado por bloqueio explícito de escrita.', {
336
+ action: 'send_group_blocked_explicit',
337
+ groupJid: normalizedJid,
338
+ sessionId: runtimeSessionId,
339
+ });
340
+ return undefined;
341
+ }
342
+
343
+ if (runtimeOptions.allowGroupWrite === true) {
344
+ resolvedGroupWritePermission = {
345
+ allowed: true,
346
+ ownerSessionId: runtimeSessionId,
347
+ reason: 'explicit_allow',
348
+ };
349
+ } else {
350
+ resolvedGroupWritePermission = await resolveGroupWritePermission(normalizedJid, runtimeSessionId);
351
+ if (!resolvedGroupWritePermission.allowed) {
352
+ logger.info('Envio para grupo bloqueado por sessão não-owner.', {
353
+ action: 'send_group_blocked_non_owner',
354
+ groupJid: normalizedJid,
355
+ sessionId: runtimeSessionId,
356
+ ownerSessionId: resolvedGroupWritePermission.ownerSessionId,
357
+ reason: resolvedGroupWritePermission.reason,
358
+ });
359
+ return undefined;
360
+ }
361
+ }
362
+ }
363
+
229
364
  const normalizedOptions = normalizeSendOptions(runtimeOptions.sendOptions);
230
365
  const shouldSendPresence = shouldSendReplyPresence(normalizedJid, content, runtimeOptions);
231
366
 
@@ -291,7 +426,13 @@ export async function sendAndStore(sock, jid, content, options) {
291
426
  const senderId = sock?.user?.id || sent?.key?.participant;
292
427
  if (sent?.key?.id) {
293
428
  try {
294
- queueMessageInsert(buildMessageData(sent, senderId));
429
+ const messageData = buildMessageData(sent, senderId, runtimeSessionId);
430
+ const targetGroupJid = normalizeJid(messageData.chat_id || normalizedJid);
431
+ if (isGroupJid(targetGroupJid)) {
432
+ const allowGroupWrite = runtimeOptions.allowGroupWrite === true || resolvedGroupWritePermission?.allowed === true;
433
+ messageData.allow_group_write = allowGroupWrite;
434
+ }
435
+ queueMessageInsert(messageData);
295
436
  } catch (error) {
296
437
  logger.warn('Falha ao enfileirar mensagem enviada para persistencia.', {
297
438
  error: error.message,
@@ -0,0 +1,157 @@
1
+ const DEFAULT_SESSION_ID = 'default';
2
+ const SESSION_ID_MAX_LENGTH = 64;
3
+ const SESSION_ID_PATTERN = /^[a-zA-Z0-9:_-]+$/;
4
+ const OWNER_ENFORCEMENT_MODES = new Set(['off', 'shadow', 'enforce']);
5
+
6
+ const parseEnvBool = (value, fallback) => {
7
+ if (value === undefined || value === null || value === '') return fallback;
8
+ const normalized = String(value).trim().toLowerCase();
9
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
10
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
11
+ return fallback;
12
+ };
13
+
14
+ const parseEnvInt = (value, fallback, min, max) => {
15
+ const parsed = Number(value);
16
+ if (!Number.isFinite(parsed)) return fallback;
17
+ return Math.max(min, Math.min(max, Math.floor(parsed)));
18
+ };
19
+
20
+ const parseFlexibleEntries = (value) =>
21
+ String(value || '')
22
+ .split(/[,\n;]+/g)
23
+ .map((entry) => String(entry || '').trim())
24
+ .filter(Boolean);
25
+
26
+ const normalizeSessionId = (value) => String(value || '').trim();
27
+
28
+ const isValidSessionId = (value) => {
29
+ const normalized = normalizeSessionId(value);
30
+ if (!normalized) return false;
31
+ if (normalized.length > SESSION_ID_MAX_LENGTH) return false;
32
+ return SESSION_ID_PATTERN.test(normalized);
33
+ };
34
+
35
+ const parseSessionIds = ({ sessionIdsRaw = '', legacySessionIdRaw = '' } = {}) => {
36
+ const warnings = [];
37
+ const validSessionIds = [];
38
+ const seen = new Set();
39
+
40
+ const legacySessionId = normalizeSessionId(legacySessionIdRaw) || DEFAULT_SESSION_ID;
41
+ const requestedSessionIds = parseFlexibleEntries(sessionIdsRaw);
42
+ const sourceSessionIds = requestedSessionIds.length > 0 ? requestedSessionIds : [legacySessionId];
43
+
44
+ for (const candidate of sourceSessionIds) {
45
+ const sessionId = normalizeSessionId(candidate);
46
+ if (!isValidSessionId(sessionId)) {
47
+ warnings.push(`session_id invalido ignorado: "${candidate}"`);
48
+ continue;
49
+ }
50
+ if (seen.has(sessionId)) continue;
51
+ seen.add(sessionId);
52
+ validSessionIds.push(sessionId);
53
+ }
54
+
55
+ if (validSessionIds.length === 0) {
56
+ validSessionIds.push(DEFAULT_SESSION_ID);
57
+ warnings.push(`nenhum session_id valido encontrado; usando fallback "${DEFAULT_SESSION_ID}"`);
58
+ }
59
+
60
+ return {
61
+ sessionIds: validSessionIds,
62
+ warnings,
63
+ };
64
+ };
65
+
66
+ const parseSessionWeights = (rawValue, sessionIds, warnings) => {
67
+ const allowedSessions = new Set(sessionIds);
68
+ const weights = {};
69
+ for (const sessionId of sessionIds) {
70
+ weights[sessionId] = 1;
71
+ }
72
+
73
+ const entries = parseFlexibleEntries(rawValue);
74
+ if (entries.length === 0) return weights;
75
+
76
+ for (const entry of entries) {
77
+ const separator = entry.includes('=') ? '=' : entry.includes(':') ? ':' : '';
78
+ if (!separator) {
79
+ warnings.push(`peso de sessao invalido (faltando separador "=" ou ":"): "${entry}"`);
80
+ continue;
81
+ }
82
+
83
+ const [rawSessionId, rawWeight] = entry.split(separator, 2);
84
+ const sessionId = normalizeSessionId(rawSessionId);
85
+ if (!isValidSessionId(sessionId)) {
86
+ warnings.push(`peso ignorado para session_id invalido: "${rawSessionId}"`);
87
+ continue;
88
+ }
89
+ if (!allowedSessions.has(sessionId)) {
90
+ warnings.push(`peso ignorado para session_id nao listado em BAILEYS_SESSION_IDS: "${sessionId}"`);
91
+ continue;
92
+ }
93
+
94
+ const weight = parseEnvInt(rawWeight, Number.NaN, 1, 1000);
95
+ if (!Number.isFinite(weight)) {
96
+ warnings.push(`peso invalido para "${sessionId}": "${rawWeight}"`);
97
+ continue;
98
+ }
99
+ weights[sessionId] = weight;
100
+ }
101
+
102
+ return weights;
103
+ };
104
+
105
+ export const resolveMultiSessionRuntimeConfig = (env = process.env) => {
106
+ const warnings = [];
107
+ const legacySessionId = normalizeSessionId(env.BAILEYS_AUTH_SESSION_ID) || DEFAULT_SESSION_ID;
108
+ const { sessionIds, warnings: parseWarnings } = parseSessionIds({
109
+ sessionIdsRaw: env.BAILEYS_SESSION_IDS,
110
+ legacySessionIdRaw: legacySessionId,
111
+ });
112
+ warnings.push(...parseWarnings);
113
+
114
+ const requestedPrimary = normalizeSessionId(env.BAILEYS_PRIMARY_SESSION_ID);
115
+ let primarySessionId = requestedPrimary || sessionIds[0];
116
+
117
+ if (requestedPrimary && !isValidSessionId(requestedPrimary)) {
118
+ warnings.push(`BAILEYS_PRIMARY_SESSION_ID invalido: "${requestedPrimary}"`);
119
+ primarySessionId = sessionIds[0];
120
+ } else if (requestedPrimary && !sessionIds.includes(requestedPrimary)) {
121
+ warnings.push(`BAILEYS_PRIMARY_SESSION_ID fora da lista de sessoes: "${requestedPrimary}"`);
122
+ primarySessionId = sessionIds[0];
123
+ }
124
+
125
+ const ownerEnforcementModeRaw = String(env.GROUP_OWNER_ENFORCEMENT_MODE || 'off')
126
+ .trim()
127
+ .toLowerCase();
128
+ const ownerEnforcementMode = OWNER_ENFORCEMENT_MODES.has(ownerEnforcementModeRaw) ? ownerEnforcementModeRaw : 'off';
129
+ if (!OWNER_ENFORCEMENT_MODES.has(ownerEnforcementModeRaw)) {
130
+ warnings.push(`GROUP_OWNER_ENFORCEMENT_MODE invalido: "${ownerEnforcementModeRaw}"`);
131
+ }
132
+
133
+ const ownerLeaseMs = parseEnvInt(env.GROUP_OWNER_LEASE_MS, 120_000, 5_000, 15 * 60 * 1000);
134
+ let ownerHeartbeatMs = parseEnvInt(env.GROUP_OWNER_HEARTBEAT_MS, 30_000, 1_000, 5 * 60 * 1000);
135
+ if (ownerHeartbeatMs >= ownerLeaseMs) {
136
+ ownerHeartbeatMs = Math.max(1_000, Math.floor(ownerLeaseMs / 2));
137
+ warnings.push(`GROUP_OWNER_HEARTBEAT_MS ajustado automaticamente para ${ownerHeartbeatMs}ms (precisa ser menor que lease)`);
138
+ }
139
+
140
+ const balancerEnabled = parseEnvBool(env.GROUP_BALANCER_ENABLED, false);
141
+ const sessionWeights = parseSessionWeights(env.BAILEYS_SESSION_WEIGHTS, sessionIds, warnings);
142
+
143
+ return Object.freeze({
144
+ sessionIds: Object.freeze([...sessionIds]),
145
+ primarySessionId,
146
+ sessionWeights: Object.freeze({ ...sessionWeights }),
147
+ ownerEnforcementMode,
148
+ ownerLeaseMs,
149
+ ownerHeartbeatMs,
150
+ balancerEnabled,
151
+ warnings: Object.freeze([...warnings]),
152
+ });
153
+ };
154
+
155
+ export const multiSessionRuntimeConfig = resolveMultiSessionRuntimeConfig();
156
+
157
+ export const getMultiSessionRuntimeConfig = () => multiSessionRuntimeConfig;
@@ -6,7 +6,7 @@ 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
+ const PINNED_BAILEYS_REF = 'github:jlucaso1/Baileys#feat-add-stickerpack-support';
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
  const baileysPackageJsonPath = require.resolve('@whiskeysockets/baileys/package.json');
@@ -0,0 +1,109 @@
1
+ export const normalizeAssignmentVersion = (value) => {
2
+ const parsed = Number.parseInt(String(value ?? ''), 10);
3
+ if (!Number.isFinite(parsed) || parsed <= 0) return null;
4
+ return parsed;
5
+ };
6
+
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
+ ) => {
28
+ const safeGroupJid = String(groupJid || '').trim();
29
+ const safeSessionId = normalizeSessionIdImpl(sessionId);
30
+ if (!safeGroupJid || !isGroupJidImpl(safeGroupJid)) {
31
+ return {
32
+ allowed: true,
33
+ ownerSessionId: null,
34
+ assignmentVersion: null,
35
+ reason: 'not_group',
36
+ };
37
+ }
38
+
39
+ const safeExpectedAssignmentVersion = normalizeAssignmentVersion(expectedAssignmentVersion);
40
+ const cacheKey = buildCacheKeyImpl(safeGroupJid, safeSessionId);
41
+ const mustBypassCache = bypassCache || Boolean(enforceFence && safeExpectedAssignmentVersion);
42
+
43
+ if (!mustBypassCache && cacheKey) {
44
+ const cached = cacheImpl.get(cacheKey);
45
+ if (cached && typeof cached === 'object') {
46
+ return cached;
47
+ }
48
+ }
49
+
50
+ try {
51
+ const ownerState = await getOwnerImpl(safeGroupJid, { bypassCache: mustBypassCache });
52
+ let ownerSessionId = String(ownerState?.ownerSessionId || '').trim() || null;
53
+ let assignmentVersion = normalizeAssignmentVersion(ownerState?.assignmentVersion);
54
+ let allowed = false;
55
+ let reason = 'owned_by_other';
56
+
57
+ if (!ownerSessionId && allowClaim) {
58
+ const claimOutcome = await tryAcquireImpl({
59
+ groupJid: safeGroupJid,
60
+ sessionId: safeSessionId,
61
+ reason: 'writer_gate_claim',
62
+ changedBy: safeSessionId,
63
+ metadata: {
64
+ source,
65
+ gate: 'group_write',
66
+ },
67
+ });
68
+
69
+ ownerSessionId = String(claimOutcome?.owner?.ownerSessionId || '').trim() || null;
70
+ assignmentVersion = normalizeAssignmentVersion(claimOutcome?.assignmentVersion ?? claimOutcome?.owner?.assignmentVersion);
71
+ allowed = Boolean(claimOutcome?.acquired && ownerSessionId === safeSessionId);
72
+ reason = claimOutcome?.reason || 'claim_attempt';
73
+ } else {
74
+ allowed = Boolean(ownerSessionId && ownerSessionId === safeSessionId);
75
+ reason = !ownerSessionId ? 'owner_missing' : allowed ? 'owner_match' : 'owned_by_other';
76
+ }
77
+
78
+ if (allowed && enforceFence && safeExpectedAssignmentVersion && assignmentVersion && assignmentVersion !== safeExpectedAssignmentVersion) {
79
+ allowed = false;
80
+ reason = 'fence_token_mismatch';
81
+ }
82
+
83
+ const resolved = {
84
+ allowed,
85
+ ownerSessionId,
86
+ assignmentVersion,
87
+ reason,
88
+ };
89
+
90
+ if (cacheKey) {
91
+ cacheImpl.set(cacheKey, resolved);
92
+ }
93
+ return resolved;
94
+ } catch (error) {
95
+ loggerImpl.warn('Falha ao resolver ownership para escrita de grupo.', {
96
+ action: 'group_owner_write_state_failed',
97
+ source,
98
+ sessionId: safeSessionId,
99
+ groupId: safeGroupJid,
100
+ error: error?.message,
101
+ });
102
+ return {
103
+ allowed: false,
104
+ ownerSessionId: null,
105
+ assignmentVersion: null,
106
+ reason: 'owner_resolution_failed',
107
+ };
108
+ }
109
+ };