@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
@@ -0,0 +1,309 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { createGroupOwnershipService } from './groupOwnershipService.js';
5
+ import { closePool } from '../../../database/index.js';
6
+
7
+ const cloneDate = (value) => (value instanceof Date ? new Date(value.getTime()) : null);
8
+
9
+ const createInMemoryRepository = () => {
10
+ const assignments = new Map();
11
+ const history = [];
12
+
13
+ const normalizeGroupJid = (value) => {
14
+ const normalized = String(value || '').trim().slice(0, 255);
15
+ return normalized || null;
16
+ };
17
+ const normalizeSessionId = (value) => {
18
+ const normalized = String(value || '').trim().slice(0, 64);
19
+ return normalized || null;
20
+ };
21
+ const normalizeReason = (value) => {
22
+ const normalized = String(value || '').trim().slice(0, 64);
23
+ return normalized || null;
24
+ };
25
+ const normalizeChangedBy = (value) => {
26
+ const normalized = String(value || 'system').trim().slice(0, 64);
27
+ return normalized || 'system';
28
+ };
29
+
30
+ const cloneAssignment = (row) => {
31
+ if (!row) return null;
32
+ return {
33
+ groupJid: row.groupJid,
34
+ ownerSessionId: row.ownerSessionId,
35
+ leaseExpiresAt: cloneDate(row.leaseExpiresAt),
36
+ cooldownUntil: cloneDate(row.cooldownUntil),
37
+ assignmentVersion: Number(row.assignmentVersion || 1),
38
+ pinned: row.pinned === true,
39
+ lastReason: row.lastReason || null,
40
+ createdAt: cloneDate(row.createdAt),
41
+ updatedAt: cloneDate(row.updatedAt),
42
+ };
43
+ };
44
+
45
+ const upsertAssignment = (assignment) => {
46
+ const now = new Date();
47
+ const current = assignments.get(assignment.groupJid);
48
+ const next = {
49
+ groupJid: assignment.groupJid,
50
+ ownerSessionId: assignment.ownerSessionId,
51
+ leaseExpiresAt: cloneDate(assignment.leaseExpiresAt),
52
+ cooldownUntil: cloneDate(assignment.cooldownUntil),
53
+ assignmentVersion: Number(assignment.assignmentVersion || 1),
54
+ pinned: assignment.pinned === true,
55
+ lastReason: assignment.lastReason || null,
56
+ createdAt: current?.createdAt ? cloneDate(current.createdAt) : now,
57
+ updatedAt: now,
58
+ };
59
+ assignments.set(next.groupJid, next);
60
+ return cloneAssignment(next);
61
+ };
62
+
63
+ return {
64
+ normalizeGroupJid,
65
+ normalizeSessionId,
66
+ normalizeReason,
67
+ normalizeChangedBy,
68
+ getAssignment: async (groupJid) => cloneAssignment(assignments.get(normalizeGroupJid(groupJid))),
69
+ getAssignmentForUpdate: async (groupJid) => cloneAssignment(assignments.get(normalizeGroupJid(groupJid))),
70
+ listAssignments: async ({ groupJid = null, ownerSessionId = null, includeExpired = true, limit = 200 } = {}) => {
71
+ const safeGroupJid = normalizeGroupJid(groupJid);
72
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
73
+ const nowMs = Date.now();
74
+ const rows = Array.from(assignments.values())
75
+ .filter((row) => (safeGroupJid ? row.groupJid === safeGroupJid : true))
76
+ .filter((row) => (safeOwnerSessionId ? row.ownerSessionId === safeOwnerSessionId : true))
77
+ .filter((row) => (includeExpired ? true : row.leaseExpiresAt?.getTime?.() > nowMs))
78
+ .slice(0, Math.max(1, Number(limit || 200)));
79
+ return rows.map((row) => cloneAssignment(row));
80
+ },
81
+ createAssignment: async ({ groupJid, ownerSessionId, leaseExpiresAt, cooldownUntil = null, pinned = false, reason = null, assignmentVersion = 1 } = {}) => {
82
+ const safeGroupJid = normalizeGroupJid(groupJid);
83
+ if (!safeGroupJid) {
84
+ throw new Error('groupJid invalido');
85
+ }
86
+ if (assignments.has(safeGroupJid)) {
87
+ const error = new Error('duplicate');
88
+ error.code = 'ER_DUP_ENTRY';
89
+ throw error;
90
+ }
91
+ return upsertAssignment({
92
+ groupJid: safeGroupJid,
93
+ ownerSessionId: normalizeSessionId(ownerSessionId),
94
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
95
+ cooldownUntil: cloneDate(cooldownUntil),
96
+ assignmentVersion: Number(assignmentVersion || 1),
97
+ pinned: pinned === true,
98
+ lastReason: normalizeReason(reason),
99
+ });
100
+ },
101
+ updateAssignmentOwner: async ({ groupJid, ownerSessionId, leaseExpiresAt, reason = null, bumpVersion = true, cooldownUntil = undefined, pinned = undefined } = {}) => {
102
+ const safeGroupJid = normalizeGroupJid(groupJid);
103
+ const current = assignments.get(safeGroupJid);
104
+ if (!current) return null;
105
+ return upsertAssignment({
106
+ ...current,
107
+ ownerSessionId: normalizeSessionId(ownerSessionId) || current.ownerSessionId,
108
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
109
+ cooldownUntil: cooldownUntil === undefined ? current.cooldownUntil : cloneDate(cooldownUntil),
110
+ pinned: pinned === undefined ? current.pinned : pinned === true,
111
+ lastReason: normalizeReason(reason),
112
+ assignmentVersion: bumpVersion ? Number(current.assignmentVersion || 1) + 1 : Number(current.assignmentVersion || 1),
113
+ });
114
+ },
115
+ updateAssignmentLease: async ({ groupJid, ownerSessionId, leaseExpiresAt, reason = undefined } = {}) => {
116
+ const safeGroupJid = normalizeGroupJid(groupJid);
117
+ const current = assignments.get(safeGroupJid);
118
+ if (!current) return null;
119
+ if (current.ownerSessionId !== normalizeSessionId(ownerSessionId)) return cloneAssignment(current);
120
+ return upsertAssignment({
121
+ ...current,
122
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
123
+ lastReason: reason === undefined ? current.lastReason : normalizeReason(reason),
124
+ });
125
+ },
126
+ expireAssignment: async ({ groupJid, ownerSessionId = null, reason = null, bumpVersion = true, leaseExpiresAt = new Date() } = {}) => {
127
+ const safeGroupJid = normalizeGroupJid(groupJid);
128
+ const current = assignments.get(safeGroupJid);
129
+ if (!current) return null;
130
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
131
+ if (safeOwnerSessionId && current.ownerSessionId !== safeOwnerSessionId) return cloneAssignment(current);
132
+ return upsertAssignment({
133
+ ...current,
134
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
135
+ lastReason: normalizeReason(reason),
136
+ assignmentVersion: bumpVersion ? Number(current.assignmentVersion || 1) + 1 : Number(current.assignmentVersion || 1),
137
+ });
138
+ },
139
+ renewLeasesByOwner: async ({ ownerSessionId, leaseExpiresAt, reason = null, now = undefined } = {}) => {
140
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
141
+ const safeNow = now instanceof Date ? now.getTime() : Date.now();
142
+ let renewed = 0;
143
+ for (const current of assignments.values()) {
144
+ if (current.ownerSessionId !== safeOwnerSessionId) continue;
145
+ if ((current.leaseExpiresAt?.getTime?.() || 0) <= safeNow) continue;
146
+ upsertAssignment({
147
+ ...current,
148
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
149
+ lastReason: normalizeReason(reason),
150
+ });
151
+ renewed += 1;
152
+ }
153
+ return renewed;
154
+ },
155
+ insertAssignmentHistory: async ({ groupJid, previousSessionId = null, newSessionId, changeReason = null, changedBy = 'system', assignmentVersion = 1, metadata = null } = {}) => {
156
+ history.push({
157
+ groupJid: normalizeGroupJid(groupJid),
158
+ previousSessionId: normalizeSessionId(previousSessionId),
159
+ newSessionId: normalizeSessionId(newSessionId),
160
+ changeReason: normalizeReason(changeReason),
161
+ changedBy: normalizeChangedBy(changedBy),
162
+ assignmentVersion: Number(assignmentVersion || 1),
163
+ metadata,
164
+ });
165
+ return { id: history.length };
166
+ },
167
+ __state: {
168
+ assignments,
169
+ history,
170
+ },
171
+ };
172
+ };
173
+
174
+ const createSessionRegistryMock = () => ({
175
+ ensureSession: async () => ({ ok: true }),
176
+ heartbeatSession: async () => ({ ok: true }),
177
+ });
178
+
179
+ const createService = ({ nowRef }) => {
180
+ const repository = createInMemoryRepository();
181
+ const sessionRegistry = createSessionRegistryMock();
182
+ const service = createGroupOwnershipService({
183
+ repository,
184
+ sessionRegistry,
185
+ withTransactionImpl: async (handler) => handler({}),
186
+ nowImpl: () => nowRef.value,
187
+ loggerImpl: { warn: () => {} },
188
+ cacheTtlMs: 1,
189
+ });
190
+ return { service, repository };
191
+ };
192
+
193
+ test.after(async () => {
194
+ await new Promise((resolve) => {
195
+ setTimeout(resolve, 200);
196
+ });
197
+ await closePool();
198
+ });
199
+
200
+ test('groupOwnershipService: claim concorrente no mesmo grupo resulta em owner unico', async () => {
201
+ const nowRef = { value: 1_000 };
202
+ const { service } = createService({ nowRef });
203
+
204
+ const [left, right] = await Promise.all([
205
+ service.tryAcquire({
206
+ groupJid: '120363222222222222@g.us',
207
+ sessionId: 'session-a',
208
+ reason: 'claim_a',
209
+ }),
210
+ service.tryAcquire({
211
+ groupJid: '120363222222222222@g.us',
212
+ sessionId: 'session-b',
213
+ reason: 'claim_b',
214
+ }),
215
+ ]);
216
+
217
+ const acquiredCount = [left, right].filter((item) => item?.acquired).length;
218
+ assert.equal(acquiredCount, 1);
219
+
220
+ const owner = await service.getOwner('120363222222222222@g.us', { bypassCache: true });
221
+ assert.ok(owner);
222
+ assert.equal(owner.assignmentVersion, 1);
223
+ assert.ok(owner.ownerSessionId === 'session-a' || owner.ownerSessionId === 'session-b');
224
+ });
225
+
226
+ test('groupOwnershipService: heartbeat renova lease e failover ocorre apos expirar', async () => {
227
+ const nowRef = { value: 10_000 };
228
+ const { service } = createService({ nowRef });
229
+ const groupJid = '120363333333333333@g.us';
230
+
231
+ const firstClaim = await service.tryAcquire({
232
+ groupJid,
233
+ sessionId: 'session-a',
234
+ leaseMs: 2_000,
235
+ });
236
+ assert.equal(firstClaim.acquired, true);
237
+ assert.equal(firstClaim.assignmentVersion, 1);
238
+
239
+ nowRef.value += 1_000;
240
+ const heartbeat = await service.heartbeatOwnerSession({
241
+ sessionId: 'session-a',
242
+ leaseMs: 2_000,
243
+ reason: 'test_heartbeat',
244
+ });
245
+ assert.ok(heartbeat.renewedAssignments >= 1);
246
+
247
+ nowRef.value = heartbeat.leaseExpiresAt.getTime() + 10;
248
+ const failover = await service.tryAcquire({
249
+ groupJid,
250
+ sessionId: 'session-b',
251
+ leaseMs: 2_000,
252
+ reason: 'failover_after_expiry',
253
+ });
254
+
255
+ assert.equal(failover.acquired, true);
256
+ assert.equal(failover.reason, 'reassigned');
257
+ assert.equal(failover.assignmentVersion, 2);
258
+
259
+ const owner = await service.getOwner(groupJid, { bypassCache: true });
260
+ assert.equal(owner?.ownerSessionId, 'session-b');
261
+ assert.equal(owner?.assignmentVersion, 2);
262
+ });
263
+
264
+ test('groupOwnershipService: fence token com assignment_version invalida sessao com token antigo', async () => {
265
+ const nowRef = { value: 50_000 };
266
+ const { service } = createService({ nowRef });
267
+ const groupJid = '120363444444444444@g.us';
268
+
269
+ const claimed = await service.tryAcquire({
270
+ groupJid,
271
+ sessionId: 'session-a',
272
+ leaseMs: 5_000,
273
+ reason: 'initial_claim',
274
+ });
275
+ assert.equal(claimed.acquired, true);
276
+ assert.equal(claimed.assignmentVersion, 1);
277
+
278
+ const tokenBefore = service.buildFencingToken({
279
+ groupJid,
280
+ ownerSessionId: 'session-a',
281
+ assignmentVersion: 1,
282
+ });
283
+ assert.equal(tokenBefore, `${groupJid}:session-a:1`);
284
+
285
+ const forced = await service.forceAssign({
286
+ groupJid,
287
+ sessionId: 'session-b',
288
+ reason: 'forced_failover',
289
+ changedBy: 'test',
290
+ });
291
+ assert.equal(forced.reassigned, true);
292
+ assert.equal(forced.assignmentVersion, 2);
293
+
294
+ const oldTokenValidation = await service.validateFenceToken({
295
+ groupJid,
296
+ sessionId: 'session-a',
297
+ assignmentVersion: 1,
298
+ bypassCache: true,
299
+ });
300
+ assert.equal(oldTokenValidation.valid, false);
301
+
302
+ const newTokenValidation = await service.validateFenceToken({
303
+ groupJid,
304
+ sessionId: 'session-b',
305
+ assignmentVersion: 2,
306
+ bypassCache: true,
307
+ });
308
+ assert.equal(newTokenValidation.valid, true);
309
+ });
@@ -0,0 +1,293 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+ import { normalizeSessionId } from './groupOwnershipRepository.js';
3
+
4
+ const SESSION_REGISTRY_TABLE = TABLES.WA_SESSION_REGISTRY;
5
+ const MAX_STATUS_LENGTH = 24;
6
+ const MAX_BOT_JID_LENGTH = 255;
7
+ const DEFAULT_STATUS = 'offline';
8
+ const DEFAULT_WEIGHT = 1;
9
+
10
+ const toDateOrNull = (value) => {
11
+ if (!value) return null;
12
+ if (value instanceof Date) {
13
+ return Number.isNaN(value.getTime()) ? null : value;
14
+ }
15
+ const parsed = new Date(value);
16
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
17
+ };
18
+
19
+ const toPositiveInt = (value, fallback = DEFAULT_WEIGHT, min = 1, max = 10_000) => {
20
+ const parsed = Number.parseInt(String(value ?? ''), 10);
21
+ if (!Number.isFinite(parsed)) return fallback;
22
+ return Math.max(min, Math.min(max, parsed));
23
+ };
24
+
25
+ const toNumber = (value, fallback = 0) => {
26
+ const parsed = Number(value);
27
+ return Number.isFinite(parsed) ? parsed : fallback;
28
+ };
29
+
30
+ const normalizeStatus = (value, fallback = DEFAULT_STATUS) => {
31
+ const normalized = String(value || fallback)
32
+ .trim()
33
+ .toLowerCase()
34
+ .slice(0, MAX_STATUS_LENGTH);
35
+ return normalized || fallback;
36
+ };
37
+
38
+ const normalizeBotJid = (value) => {
39
+ if (value === undefined) return undefined;
40
+ if (value === null) return null;
41
+ const normalized = String(value)
42
+ .trim()
43
+ .toLowerCase()
44
+ .slice(0, MAX_BOT_JID_LENGTH);
45
+ return normalized || null;
46
+ };
47
+
48
+ const parseJson = (value) => {
49
+ if (!value) return null;
50
+ if (typeof value === 'object') return value;
51
+ try {
52
+ return JSON.parse(String(value));
53
+ } catch {
54
+ return null;
55
+ }
56
+ };
57
+
58
+ const serializeJson = (value) => {
59
+ if (value === undefined) return null;
60
+ if (value === null) return null;
61
+ if (typeof value === 'string') {
62
+ const normalized = value.trim();
63
+ return normalized || null;
64
+ }
65
+ try {
66
+ return JSON.stringify(value);
67
+ } catch {
68
+ return null;
69
+ }
70
+ };
71
+
72
+ const normalizeSessionRow = (row = null) => {
73
+ if (!row) return null;
74
+ return {
75
+ sessionId: normalizeSessionId(row.session_id),
76
+ botJid: normalizeBotJid(row.bot_jid) ?? null,
77
+ status: normalizeStatus(row.status, DEFAULT_STATUS),
78
+ capacityWeight: toPositiveInt(row.capacity_weight, DEFAULT_WEIGHT),
79
+ currentScore: toNumber(row.current_score, 0),
80
+ lastHeartbeatAt: toDateOrNull(row.last_heartbeat_at),
81
+ lastConnectedAt: toDateOrNull(row.last_connected_at),
82
+ lastDisconnectedAt: toDateOrNull(row.last_disconnected_at),
83
+ metadata: parseJson(row.metadata),
84
+ createdAt: toDateOrNull(row.created_at),
85
+ updatedAt: toDateOrNull(row.updated_at),
86
+ };
87
+ };
88
+
89
+ const SESSION_SELECT_COLUMNS = `session_id,
90
+ bot_jid,
91
+ status,
92
+ capacity_weight,
93
+ current_score,
94
+ last_heartbeat_at,
95
+ last_connected_at,
96
+ last_disconnected_at,
97
+ metadata,
98
+ created_at,
99
+ updated_at`;
100
+
101
+ export const getSession = async (sessionId, { connection = null } = {}) => {
102
+ const safeSessionId = normalizeSessionId(sessionId);
103
+ if (!safeSessionId) return null;
104
+
105
+ const rows = await executeQuery(
106
+ `SELECT ${SESSION_SELECT_COLUMNS}
107
+ FROM ${SESSION_REGISTRY_TABLE}
108
+ WHERE session_id = ?
109
+ LIMIT 1`,
110
+ [safeSessionId],
111
+ connection,
112
+ );
113
+
114
+ return normalizeSessionRow(rows?.[0] || null);
115
+ };
116
+
117
+ export const listSessions = async ({ status = null, limit = 100, connection = null } = {}) => {
118
+ const safeLimit = Math.max(1, Math.min(2_000, toPositiveInt(limit, 100, 1, 2_000)));
119
+ const safeStatus = status ? normalizeStatus(status, '') : '';
120
+
121
+ const params = [];
122
+ let where = '';
123
+ if (safeStatus) {
124
+ where = 'WHERE status = ?';
125
+ params.push(safeStatus);
126
+ }
127
+
128
+ const rows = await executeQuery(
129
+ `SELECT ${SESSION_SELECT_COLUMNS}
130
+ FROM ${SESSION_REGISTRY_TABLE}
131
+ ${where}
132
+ ORDER BY updated_at DESC
133
+ LIMIT ${safeLimit}`,
134
+ params,
135
+ connection,
136
+ );
137
+
138
+ return (Array.isArray(rows) ? rows : []).map((row) => normalizeSessionRow(row));
139
+ };
140
+
141
+ export const upsertSession = async (
142
+ {
143
+ sessionId,
144
+ botJid = undefined,
145
+ status = DEFAULT_STATUS,
146
+ capacityWeight = DEFAULT_WEIGHT,
147
+ currentScore = 0,
148
+ metadata = undefined,
149
+ heartbeatAt = undefined,
150
+ connectedAt = undefined,
151
+ disconnectedAt = undefined,
152
+ } = {},
153
+ { connection = null } = {},
154
+ ) => {
155
+ const safeSessionId = normalizeSessionId(sessionId);
156
+ if (!safeSessionId) {
157
+ throw new Error('upsertSession requer sessionId valido.');
158
+ }
159
+
160
+ const safeBotJid = normalizeBotJid(botJid);
161
+ const safeStatus = normalizeStatus(status, DEFAULT_STATUS);
162
+ const safeCapacityWeight = toPositiveInt(capacityWeight, DEFAULT_WEIGHT);
163
+ const safeCurrentScore = toNumber(currentScore, 0);
164
+ const safeMetadata = serializeJson(metadata);
165
+ const safeHeartbeatAt = heartbeatAt === undefined ? null : toDateOrNull(heartbeatAt);
166
+ const safeConnectedAt = connectedAt === undefined ? null : toDateOrNull(connectedAt);
167
+ const safeDisconnectedAt = disconnectedAt === undefined ? null : toDateOrNull(disconnectedAt);
168
+
169
+ await executeQuery(
170
+ `INSERT INTO ${SESSION_REGISTRY_TABLE}
171
+ (session_id, bot_jid, status, capacity_weight, current_score, last_heartbeat_at, last_connected_at, last_disconnected_at, metadata)
172
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
173
+ ON DUPLICATE KEY UPDATE
174
+ bot_jid = COALESCE(VALUES(bot_jid), bot_jid),
175
+ status = VALUES(status),
176
+ capacity_weight = VALUES(capacity_weight),
177
+ current_score = VALUES(current_score),
178
+ last_heartbeat_at = COALESCE(VALUES(last_heartbeat_at), last_heartbeat_at),
179
+ last_connected_at = COALESCE(VALUES(last_connected_at), last_connected_at),
180
+ last_disconnected_at = COALESCE(VALUES(last_disconnected_at), last_disconnected_at),
181
+ metadata = COALESCE(VALUES(metadata), metadata),
182
+ updated_at = CURRENT_TIMESTAMP`,
183
+ [safeSessionId, safeBotJid, safeStatus, safeCapacityWeight, safeCurrentScore, safeHeartbeatAt, safeConnectedAt, safeDisconnectedAt, safeMetadata],
184
+ connection,
185
+ );
186
+
187
+ return getSession(safeSessionId, { connection });
188
+ };
189
+
190
+ export const ensureSession = async (
191
+ sessionId,
192
+ {
193
+ status = 'online',
194
+ capacityWeight = DEFAULT_WEIGHT,
195
+ currentScore = 0,
196
+ metadata = undefined,
197
+ botJid = undefined,
198
+ connection = null,
199
+ } = {},
200
+ ) =>
201
+ upsertSession(
202
+ {
203
+ sessionId,
204
+ status,
205
+ capacityWeight,
206
+ currentScore,
207
+ metadata,
208
+ botJid,
209
+ },
210
+ { connection },
211
+ );
212
+
213
+ export const heartbeatSession = async (
214
+ sessionId,
215
+ {
216
+ status = 'online',
217
+ currentScore = 0,
218
+ metadata = undefined,
219
+ botJid = undefined,
220
+ capacityWeight = DEFAULT_WEIGHT,
221
+ connection = null,
222
+ } = {},
223
+ ) =>
224
+ upsertSession(
225
+ {
226
+ sessionId,
227
+ status,
228
+ currentScore,
229
+ metadata,
230
+ botJid,
231
+ capacityWeight,
232
+ heartbeatAt: new Date(),
233
+ },
234
+ { connection },
235
+ );
236
+
237
+ export const markSessionConnected = async (
238
+ sessionId,
239
+ {
240
+ botJid = undefined,
241
+ currentScore = 0,
242
+ metadata = undefined,
243
+ capacityWeight = DEFAULT_WEIGHT,
244
+ connection = null,
245
+ } = {},
246
+ ) =>
247
+ upsertSession(
248
+ {
249
+ sessionId,
250
+ botJid,
251
+ status: 'online',
252
+ currentScore,
253
+ metadata,
254
+ capacityWeight,
255
+ heartbeatAt: new Date(),
256
+ connectedAt: new Date(),
257
+ },
258
+ { connection },
259
+ );
260
+
261
+ export const markSessionDisconnected = async (
262
+ sessionId,
263
+ {
264
+ status = 'offline',
265
+ currentScore = 0,
266
+ metadata = undefined,
267
+ capacityWeight = DEFAULT_WEIGHT,
268
+ connection = null,
269
+ } = {},
270
+ ) =>
271
+ upsertSession(
272
+ {
273
+ sessionId,
274
+ status,
275
+ currentScore,
276
+ metadata,
277
+ capacityWeight,
278
+ disconnectedAt: new Date(),
279
+ },
280
+ { connection },
281
+ );
282
+
283
+ const sessionRegistryService = {
284
+ getSession,
285
+ listSessions,
286
+ upsertSession,
287
+ ensureSession,
288
+ heartbeatSession,
289
+ markSessionConnected,
290
+ markSessionDisconnected,
291
+ };
292
+
293
+ export default sessionRegistryService;
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { jidNormalizedUser } from '@whiskeysockets/baileys';
2
3
 
3
4
  const normalizeJid = (jid) => {
@@ -150,14 +151,14 @@ export const clampStickerFocusChatWindowMinutes = (value, fallback = DEFAULT_STI
150
151
 
151
152
  export const minutesToMs = (minutes) => Math.max(0, Math.floor(Number(minutes) || 0) * 60 * 1000);
152
153
 
153
- export const resolveStickerFocusState = (groupConfig = {}, now = Date.now()) => {
154
+ export const resolveStickerFocusState = (groupConfig = {}, now = __timeNowMs()) => {
154
155
  const rawCooldown = groupConfig?.stickerFocusMessageCooldownMinutes ?? groupConfig?.stickerFocusTextCooldownMinutes;
155
156
  const messageCooldownMinutes = clampStickerFocusMessageCooldownMinutes(rawCooldown);
156
157
  const rawAllowance = groupConfig?.stickerFocusMessageAllowance ?? groupConfig?.stickerFocusMessageAllowanceCount;
157
158
  const messageAllowanceCount = clampStickerFocusMessageAllowance(rawAllowance);
158
159
  const messageCooldownMs = minutesToMs(messageCooldownMinutes);
159
160
  const chatWindowUntilMs = parseTimestampMs(groupConfig?.stickerFocusChatWindowUntilMs);
160
- const safeNow = Number.isFinite(now) ? now : Date.now();
161
+ const safeNow = Number.isFinite(now) ? now : __timeNowMs();
161
162
  const chatWindowRemainingMs = Math.max(0, chatWindowUntilMs - safeNow);
162
163
 
163
164
  return {
@@ -245,7 +246,7 @@ const normalizeAllowanceHistory = (historyValue) => {
245
246
  return [];
246
247
  };
247
248
 
248
- export const canSendMessageInStickerFocus = ({ groupId, senderJid, messageCooldownMs, messageAllowanceCount = DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE, now = Date.now() }) => {
249
+ export const canSendMessageInStickerFocus = ({ groupId, senderJid, messageCooldownMs, messageAllowanceCount = DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE, now = __timeNowMs() }) => {
249
250
  const senderKey = buildSenderKey({ groupId, senderJid });
250
251
  if (!senderKey) {
251
252
  return {
@@ -268,7 +269,7 @@ export const canSendMessageInStickerFocus = ({ groupId, senderJid, messageCooldo
268
269
  };
269
270
  }
270
271
 
271
- const safeNow = Number.isFinite(now) ? now : Date.now();
272
+ const safeNow = Number.isFinite(now) ? now : __timeNowMs();
272
273
  const history = normalizeAllowanceHistory(sharedMessageAllowance.get(senderKey)).filter((timestamp) => safeNow - timestamp < normalizedCooldownMs);
273
274
  if (history.length > 0) {
274
275
  sharedMessageAllowance.set(senderKey, history);
@@ -294,12 +295,12 @@ export const canSendMessageInStickerFocus = ({ groupId, senderJid, messageCooldo
294
295
  };
295
296
  };
296
297
 
297
- export const registerMessageUsageInStickerFocus = ({ groupId, senderJid, messageCooldownMs = minutesToMs(DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES), messageAllowanceCount = DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE, now = Date.now() }) => {
298
+ export const registerMessageUsageInStickerFocus = ({ groupId, senderJid, messageCooldownMs = minutesToMs(DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES), messageAllowanceCount = DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE, now = __timeNowMs() }) => {
298
299
  const senderKey = buildSenderKey({ groupId, senderJid });
299
300
  if (!senderKey) return;
300
301
  const normalizedCooldownMs = Math.max(0, Math.floor(Number(messageCooldownMs) || 0));
301
302
  const normalizedAllowanceCount = clampStickerFocusMessageAllowance(messageAllowanceCount);
302
- const safeNow = Number.isFinite(now) ? now : Date.now();
303
+ const safeNow = Number.isFinite(now) ? now : __timeNowMs();
303
304
  const history = normalizeAllowanceHistory(sharedMessageAllowance.get(senderKey));
304
305
  const recentHistory = normalizedCooldownMs > 0 ? history.filter((timestamp) => safeNow - timestamp < normalizedCooldownMs) : history;
305
306
  recentHistory.push(safeNow);
@@ -307,10 +308,10 @@ export const registerMessageUsageInStickerFocus = ({ groupId, senderJid, message
307
308
  sharedMessageAllowance.set(senderKey, trimmedHistory);
308
309
  };
309
310
 
310
- export const shouldSendStickerFocusWarning = ({ groupId, senderJid, now = Date.now() }) => {
311
+ export const shouldSendStickerFocusWarning = ({ groupId, senderJid, now = __timeNowMs() }) => {
311
312
  const senderKey = buildSenderKey({ groupId, senderJid });
312
313
  if (!senderKey) return true;
313
- const safeNow = Number.isFinite(now) ? now : Date.now();
314
+ const safeNow = Number.isFinite(now) ? now : __timeNowMs();
314
315
  const lastWarningAt = Number(sharedWarningThrottle.get(senderKey) || 0);
315
316
  if (!lastWarningAt || safeNow - lastWarningAt >= STICKER_FOCUS_WARNING_COOLDOWN_MS) {
316
317
  sharedWarningThrottle.set(senderKey, safeNow);
@@ -329,7 +330,7 @@ export const MAX_STICKER_FOCUS_TEXT_ALLOWANCE = MAX_STICKER_FOCUS_MESSAGE_ALLOWA
329
330
  export const DEFAULT_STICKER_FOCUS_TEXT_ALLOWANCE = DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE;
330
331
  export const clampStickerFocusTextAllowance = clampStickerFocusMessageAllowance;
331
332
  export const isPlainTextMessageForStickerFocus = ({ messageInfo, extractedText, mediaEntries = [] }) => resolveStickerFocusMessageClassification({ messageInfo, extractedText, mediaEntries }).messageType === 'text';
332
- export const canSendTextInStickerFocus = ({ groupId, senderJid, textCooldownMs, textAllowanceCount, now = Date.now() }) =>
333
+ export const canSendTextInStickerFocus = ({ groupId, senderJid, textCooldownMs, textAllowanceCount, now = __timeNowMs() }) =>
333
334
  canSendMessageInStickerFocus({
334
335
  groupId,
335
336
  senderJid,
@@ -337,7 +338,7 @@ export const canSendTextInStickerFocus = ({ groupId, senderJid, textCooldownMs,
337
338
  messageAllowanceCount: textAllowanceCount,
338
339
  now,
339
340
  });
340
- export const registerTextUsageInStickerFocus = ({ groupId, senderJid, textCooldownMs, textAllowanceCount, now = Date.now() }) =>
341
+ export const registerTextUsageInStickerFocus = ({ groupId, senderJid, textCooldownMs, textAllowanceCount, now = __timeNowMs() }) =>
341
342
  registerMessageUsageInStickerFocus({
342
343
  groupId,
343
344
  senderJid,