@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.
- package/.env.example +58 -13
- package/.github/workflows/ci.yml +5 -5
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/db-migration-check.yml +2 -2
- package/.github/workflows/dependency-review.yml +1 -1
- package/.github/workflows/deploy.yml +2 -2
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/security-attest-provenance.yml +2 -2
- package/.github/workflows/security-gitleaks.yml +13 -4
- package/.github/workflows/security-runner-hardening.yml +2 -2
- package/.github/workflows/security-scorecard.yml +1 -1
- package/.github/workflows/security-zap-baseline.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +2 -1
- package/.github/workflows/security-zizmor.yml +1 -1
- package/.github/workflows/wiki-sync.yml +1 -1
- package/.gitleaksignore +9 -0
- package/CODE_OF_CONDUCT.md +2 -2
- package/GEMINI.md +64 -0
- package/README.md +52 -82
- package/SECURITY.md +1 -1
- package/app/config/index.js +2 -0
- package/app/configParts/adminIdentity.js +5 -5
- package/app/configParts/baileysConfig.js +230 -58
- package/app/configParts/groupUtils.js +5 -0
- package/app/configParts/messagePersistenceService.js +145 -4
- package/app/configParts/sessionConfig.js +157 -0
- package/app/connection/baileysCompatibility.test.js +1 -1
- package/app/connection/groupOwnerWriteStateResolver.js +109 -0
- package/app/connection/socketController.js +660 -158
- package/app/connection/socketController.multiSession.test.js +108 -0
- package/app/controllers/messageController.js +1 -1
- package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
- package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
- package/app/controllers/messageProcessingPipeline.js +93 -13
- package/app/controllers/messageProcessingPipeline.test.js +200 -0
- package/app/modules/adminModule/AGENT.md +1 -1
- package/app/modules/adminModule/commandConfig.json +3318 -1347
- package/app/modules/adminModule/groupCommandHandlers.js +858 -15
- package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
- package/app/modules/adminModule/groupWarningRepository.js +152 -0
- package/app/modules/aiModule/AGENT.md +47 -30
- package/app/modules/aiModule/aiConfigRuntime.js +1 -0
- package/app/modules/aiModule/catCommand.js +135 -27
- package/app/modules/aiModule/commandConfig.json +114 -28
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
- package/app/modules/gameModule/AGENT.md +1 -1
- package/app/modules/gameModule/commandConfig.json +29 -0
- package/app/modules/menuModule/AGENT.md +1 -1
- package/app/modules/menuModule/commandConfig.json +45 -10
- package/app/modules/menuModule/menuCatalogService.js +190 -0
- package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
- package/app/modules/menuModule/menuDynamicService.js +511 -0
- package/app/modules/menuModule/menuDynamicService.test.js +141 -0
- package/app/modules/menuModule/menus.js +36 -5
- package/app/modules/playModule/AGENT.md +10 -5
- package/app/modules/playModule/commandConfig.json +140 -12
- package/app/modules/playModule/playCommand.js +1 -1417
- package/app/modules/playModule/playCommandConstants.js +80 -0
- package/app/modules/playModule/playCommandCore.js +361 -0
- package/app/modules/playModule/playCommandHandlers.js +41 -0
- package/app/modules/playModule/playCommandMediaClient.js +1872 -0
- package/app/modules/playModule/playConfigRuntime.js +245 -4
- package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/quoteModule/quoteCommand.js +3 -2
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/statsModule/rankingCommon.js +5 -4
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/addStickerMetadata.js +4 -3
- package/app/modules/stickerModule/commandConfig.json +145 -0
- package/app/modules/stickerModule/stickerCommand.js +1 -1
- package/app/modules/stickerPackModule/AGENT.md +1 -1
- package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
- package/app/modules/stickerPackModule/commandConfig.json +29 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
- package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/systemMetricsModule/pingCommand.js +6 -5
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/tiktokCommand.js +2 -1
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/userModule/userCommand.js +72 -23
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/conversationRouterService.js +4 -3
- package/app/services/ai/geminiService.js +132 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/globalModuleAiHelpService.js +3 -2
- package/app/services/ai/messageCommandExecutionService.js +2 -1
- package/app/services/ai/moduleAiHelpCoreService.js +45 -14
- package/app/services/ai/moduleToolExecutorService.js +3 -2
- package/app/services/ai/moduleToolRegistryService.js +2 -1
- package/app/services/ai/toolCandidateSelectorService.js +6 -5
- package/app/services/auth/googleWebLinkService.js +3 -2
- package/app/services/auth/whatsappLoginLinkService.js +3 -2
- package/app/services/external/pokeApiService.js +4 -3
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +57 -26
- package/app/services/infra/featureFlagService.js +2 -1
- package/app/services/messaging/captchaService.js +3 -2
- package/app/services/messaging/newsBroadcastService.js +846 -29
- package/app/services/multiSession/assignmentBalancerService.js +457 -0
- package/app/services/multiSession/groupOwnershipRepository.js +381 -0
- package/app/services/multiSession/groupOwnershipService.js +890 -0
- package/app/services/multiSession/groupOwnershipService.test.js +309 -0
- package/app/services/multiSession/sessionRegistryService.js +293 -0
- package/app/services/sticker/stickerFocusService.js +11 -10
- package/app/store/aiPromptStore.js +36 -19
- package/app/store/conversationSessionStore.js +7 -6
- package/app/store/groupConfigStore.js +41 -5
- package/app/store/premiumUserStore.js +21 -7
- package/app/utils/antiLink/antiLinkModule.js +352 -16
- package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
- package/app/workers/aiLearningWorker.js +6 -5
- package/app/workers/commandConfigEnrichmentWorker.js +4 -3
- package/database/index.js +14 -8
- package/database/migrations/20260307_d0_hardening_down.sql +1 -1
- package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
- package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
- package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
- package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
- package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
- package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
- package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
- package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
- package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
- package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
- package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
- package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
- package/database/schema.sql +102 -1
- package/docker-compose.yml +4 -1
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
- package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
- package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
- package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
- package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
- package/docs/security/omnizap-static-security-headers.conf +25 -0
- package/docs/wiki/Home.md +1 -1
- package/ecosystem.prod.config.cjs +32 -12
- package/index.js +57 -23
- package/observability/alert-rules.yml +20 -0
- package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
- package/observability/mysql-setup.sql +4 -4
- package/observability/system-admin-observability.md +26 -0
- package/package.json +20 -6
- package/public/apple-touch-icon.png +0 -0
- package/public/comandos/commands-catalog.json +2853 -3326
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/js/apps/apiDocsApp.js +3 -2
- package/public/js/apps/commandsReactApp.js +280 -99
- package/public/js/apps/createPackApp.js +11 -10
- package/public/js/apps/homeReactApp.js +181 -130
- package/public/js/apps/loginReactApp.js +1 -1
- package/public/js/apps/stickersApp.js +263 -110
- package/public/js/apps/termsReactApp.js +73 -24
- package/public/js/apps/userApp.js +4 -3
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +355 -280
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/api-docs.html +1 -1
- package/public/pages/aup.html +2 -2
- package/public/pages/dpa.html +3 -3
- package/public/pages/licenca.html +4 -4
- package/public/pages/login.html +1 -1
- package/public/pages/notice-and-takedown.html +2 -2
- package/public/pages/politica-de-privacidade.html +6 -6
- package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
- package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
- package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
- package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
- package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
- package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
- package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
- package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
- package/public/pages/stickers-admin.html +1 -1
- package/public/pages/stickers-create.html +1 -1
- package/public/pages/stickers.html +6 -6
- package/public/pages/suboperadores.html +2 -2
- package/public/pages/termos-de-uso-texto-integral.html +6 -6
- package/public/pages/termos-de-uso.html +3 -3
- package/public/pages/user-password-reset.html +4 -5
- package/public/pages/user-systemadm.html +9 -463
- package/public/pages/user.html +2 -2
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +11 -1
- package/scripts/email-broadcast-terms-update.mjs +2 -1
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +166 -2
- package/scripts/generate-module-agents.mjs +2 -1
- package/scripts/generate-seo-satellite-pages.mjs +5 -4
- package/scripts/github-deploy-notify.mjs +2 -1
- package/scripts/github-release-notify.mjs +25 -10
- package/scripts/new-whatsapp-session.sh +317 -0
- package/scripts/release.sh +2 -19
- package/scripts/security-smoketest.mjs +6 -5
- package/scripts/security-web-surface-check.mjs +218 -0
- package/scripts/sticker-catalog-loadtest.mjs +5 -4
- package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
- package/server/auth/jwt/webJwtService.js +1 -1
- package/server/auth/stickerCatalogAuthContext.js +2 -1
- package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
- package/server/auth/userPassword/userPasswordAuthService.js +2 -1
- package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
- package/server/auth/webAccount/webAccountHandlers.js +9 -10
- package/server/controllers/admin/adminPanelHandlers.js +267 -16
- package/server/controllers/admin/systemAdminController.js +267 -0
- package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
- package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
- package/server/controllers/sticker/stickerCatalogController.js +23 -36
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/githubController.js +3 -2
- package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
- package/server/controllers/system/systemController.js +254 -1
- package/server/controllers/system/systemMetricsController.js +2 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailTemplateService.js +5 -3
- package/server/http/httpServer.js +11 -6
- package/server/middleware/rateLimit.js +2 -1
- package/server/middleware/securityHeaders.js +20 -1
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +30 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/static/staticPageRouter.js +27 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/time/timeModule.js +135 -0
- package/utils/time/timeModule.test.js +65 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +7 -1
- package/public/assets/images/brand-icon-192.png +0 -0
- 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 =
|
|
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 :
|
|
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 =
|
|
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 :
|
|
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 =
|
|
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 :
|
|
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 =
|
|
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 :
|
|
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 =
|
|
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 =
|
|
341
|
+
export const registerTextUsageInStickerFocus = ({ groupId, senderJid, textCooldownMs, textAllowanceCount, now = __timeNowMs() }) =>
|
|
341
342
|
registerMessageUsageInStickerFocus({
|
|
342
343
|
groupId,
|
|
343
344
|
senderJid,
|