@omnizap-system/omnizap 2.6.1 → 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 +54 -9
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/security-runner-hardening.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +1 -0
- package/app/config/index.js +2 -0
- package/app/configParts/adminIdentity.js +5 -5
- package/app/configParts/baileysConfig.js +226 -55
- package/app/configParts/groupUtils.js +5 -0
- package/app/configParts/messagePersistenceService.js +143 -3
- 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 +625 -124
- 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 +88 -9
- 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 +856 -14
- package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
- 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 +132 -25
- 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 +74 -16
- package/app/modules/playModule/playCommandConstants.js +13 -7
- package/app/modules/playModule/playCommandCore.js +4 -6
- package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
- package/app/modules/playModule/playConfigRuntime.js +5 -6
- package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/commandConfig.json +145 -0
- 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/stickerAutoPackByTagsRuntime.js +1 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/geminiService.js +131 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/moduleAiHelpCoreService.js +33 -4
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +51 -21
- package/app/services/messaging/newsBroadcastService.js +843 -27
- 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/store/aiPromptStore.js +36 -19
- 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/database/index.js +6 -0
- 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/privacy-policy-2026-03-07.md +2 -2
- package/docs/security/dsar-lgpd-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/ecosystem.prod.config.cjs +31 -11
- package/index.js +52 -18
- 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 +12 -5
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/stickersApp.js +255 -103
- package/public/js/apps/termsReactApp.js +57 -8
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +96 -47
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/politica-de-privacidade.html +1 -1
- package/public/pages/stickers.html +5 -5
- package/public/pages/termos-de-uso-texto-integral.html +1 -1
- package/public/pages/termos-de-uso.html +1 -1
- package/public/pages/user-password-reset.html +3 -4
- package/public/pages/user-systemadm.html +8 -462
- package/public/pages/user.html +1 -1
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +10 -0
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +155 -0
- package/scripts/new-whatsapp-session.sh +317 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +267 -0
- package/server/controllers/sticker/stickerCatalogController.js +9 -23
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
- package/server/controllers/system/systemController.js +254 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailTemplateService.js +3 -2
- package/server/http/httpServer.js +8 -4
- 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/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +2 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import logger from '#logger';
|
|
2
|
+
import { withTransaction } from '../../../database/index.js';
|
|
3
|
+
import { getMultiSessionRuntimeConfig } from '../../configParts/sessionConfig.js';
|
|
4
|
+
import * as groupOwnershipRepository from './groupOwnershipRepository.js';
|
|
5
|
+
import sessionRegistryService from './sessionRegistryService.js';
|
|
6
|
+
|
|
7
|
+
const runtimeConfig = getMultiSessionRuntimeConfig();
|
|
8
|
+
const DEFAULT_LEASE_MS = Math.max(5_000, Number(runtimeConfig?.ownerLeaseMs) || 120_000);
|
|
9
|
+
const DEFAULT_CACHE_TTL_MS = Math.max(250, Math.min(5_000, Math.floor((Number(runtimeConfig?.ownerHeartbeatMs) || 30_000) / 4)));
|
|
10
|
+
const DEFAULT_CACHE_MAX_ENTRIES = 10_000;
|
|
11
|
+
const DUPLICATE_KEY_ERRORS = new Set(['ER_DUP_ENTRY', 'ER_DUP_KEY']);
|
|
12
|
+
|
|
13
|
+
const parsePositiveInt = (value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) => {
|
|
14
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
15
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
16
|
+
return Math.max(min, Math.min(max, parsed));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const parseAssignmentVersion = (value) => {
|
|
20
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
21
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
|
22
|
+
return parsed;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const isDuplicateError = (error) => {
|
|
26
|
+
const code = String(error?.code || error?.originalError?.code || '');
|
|
27
|
+
return DUPLICATE_KEY_ERRORS.has(code);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const hasActiveLease = (assignment, nowMs) => {
|
|
31
|
+
const leaseMs = assignment?.leaseExpiresAt instanceof Date ? assignment.leaseExpiresAt.getTime() : Number.NaN;
|
|
32
|
+
return Number.isFinite(leaseMs) && leaseMs > nowMs;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const cloneDate = (value) => (value instanceof Date ? new Date(value.getTime()) : null);
|
|
36
|
+
|
|
37
|
+
const toOwnerState = (assignment, nowMs) => {
|
|
38
|
+
if (!assignment) return null;
|
|
39
|
+
if (!hasActiveLease(assignment, nowMs)) return null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
groupJid: assignment.groupJid,
|
|
43
|
+
ownerSessionId: assignment.ownerSessionId,
|
|
44
|
+
leaseExpiresAt: cloneDate(assignment.leaseExpiresAt),
|
|
45
|
+
cooldownUntil: cloneDate(assignment.cooldownUntil),
|
|
46
|
+
assignmentVersion: Number(assignment.assignmentVersion || 1),
|
|
47
|
+
pinned: assignment.pinned === true,
|
|
48
|
+
lastReason: assignment.lastReason || null,
|
|
49
|
+
createdAt: cloneDate(assignment.createdAt),
|
|
50
|
+
updatedAt: cloneDate(assignment.updatedAt),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const cloneOwnerState = (ownerState) => {
|
|
55
|
+
if (!ownerState) return null;
|
|
56
|
+
return {
|
|
57
|
+
...ownerState,
|
|
58
|
+
leaseExpiresAt: cloneDate(ownerState.leaseExpiresAt),
|
|
59
|
+
cooldownUntil: cloneDate(ownerState.cooldownUntil),
|
|
60
|
+
createdAt: cloneDate(ownerState.createdAt),
|
|
61
|
+
updatedAt: cloneDate(ownerState.updatedAt),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const cloneOutcome = (outcome = null) => {
|
|
66
|
+
if (!outcome || typeof outcome !== 'object') return outcome;
|
|
67
|
+
return {
|
|
68
|
+
...outcome,
|
|
69
|
+
owner: cloneOwnerState(outcome.owner),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const createGroupOwnershipService = ({
|
|
74
|
+
repository = groupOwnershipRepository,
|
|
75
|
+
sessionRegistry = sessionRegistryService,
|
|
76
|
+
withTransactionImpl = withTransaction,
|
|
77
|
+
nowImpl = () => Date.now(),
|
|
78
|
+
loggerImpl = logger,
|
|
79
|
+
defaultLeaseMs = DEFAULT_LEASE_MS,
|
|
80
|
+
cacheTtlMs = parsePositiveInt(process.env.GROUP_OWNER_CACHE_TTL_MS, DEFAULT_CACHE_TTL_MS, 250, 10_000),
|
|
81
|
+
cacheMaxEntries = DEFAULT_CACHE_MAX_ENTRIES,
|
|
82
|
+
} = {}) => {
|
|
83
|
+
const ownerCache = new Map();
|
|
84
|
+
const safeDefaultLeaseMs = parsePositiveInt(defaultLeaseMs, DEFAULT_LEASE_MS, 5_000, 15 * 60 * 1000);
|
|
85
|
+
const safeCacheTtlMs = parsePositiveInt(cacheTtlMs, DEFAULT_CACHE_TTL_MS, 250, 10_000);
|
|
86
|
+
const safeCacheMaxEntries = parsePositiveInt(cacheMaxEntries, DEFAULT_CACHE_MAX_ENTRIES, 10, 100_000);
|
|
87
|
+
|
|
88
|
+
const getCacheEntry = (groupJid, nowMs = nowImpl()) => {
|
|
89
|
+
const cached = ownerCache.get(groupJid);
|
|
90
|
+
if (!cached) return null;
|
|
91
|
+
if (cached.expiresAtMs <= nowMs) {
|
|
92
|
+
ownerCache.delete(groupJid);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return cached;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const setCacheEntry = (groupJid, ownerState, nowMs = nowImpl()) => {
|
|
99
|
+
ownerCache.delete(groupJid);
|
|
100
|
+
ownerCache.set(groupJid, {
|
|
101
|
+
owner: cloneOwnerState(ownerState),
|
|
102
|
+
expiresAtMs: nowMs + safeCacheTtlMs,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
while (ownerCache.size > safeCacheMaxEntries) {
|
|
106
|
+
const firstKey = ownerCache.keys().next()?.value;
|
|
107
|
+
if (!firstKey) break;
|
|
108
|
+
ownerCache.delete(firstKey);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const invalidateCache = (groupJid) => {
|
|
113
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
114
|
+
if (!safeGroupJid) return false;
|
|
115
|
+
return ownerCache.delete(safeGroupJid);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const clearCache = () => {
|
|
119
|
+
ownerCache.clear();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const resolveLeaseMs = (leaseMs) => parsePositiveInt(leaseMs, safeDefaultLeaseMs, 5_000, 15 * 60 * 1000);
|
|
123
|
+
|
|
124
|
+
const resolveHistoryVersion = async (groupJid, assignmentVersion, connection = null) => {
|
|
125
|
+
const safeVersion = Number.parseInt(String(assignmentVersion ?? ''), 10);
|
|
126
|
+
if (Number.isFinite(safeVersion) && safeVersion > 0) return safeVersion;
|
|
127
|
+
const assignment = await repository.getAssignment(groupJid, connection);
|
|
128
|
+
return Number(assignment?.assignmentVersion || 1);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const recordHistory = async (
|
|
132
|
+
{
|
|
133
|
+
groupJid,
|
|
134
|
+
previousSessionId = null,
|
|
135
|
+
newSessionId = null,
|
|
136
|
+
reason = null,
|
|
137
|
+
changedBy = 'system',
|
|
138
|
+
assignmentVersion = null,
|
|
139
|
+
metadata = null,
|
|
140
|
+
} = {},
|
|
141
|
+
connection = null,
|
|
142
|
+
) => {
|
|
143
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
144
|
+
const safePreviousSessionId = repository.normalizeSessionId(previousSessionId);
|
|
145
|
+
const safeNewSessionId = repository.normalizeSessionId(newSessionId) || safePreviousSessionId;
|
|
146
|
+
if (!safeGroupJid || !safeNewSessionId) {
|
|
147
|
+
throw new Error('recordHistory requer groupJid e newSessionId validos.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const safeVersion = await resolveHistoryVersion(safeGroupJid, assignmentVersion, connection);
|
|
151
|
+
return repository.insertAssignmentHistory(
|
|
152
|
+
{
|
|
153
|
+
groupJid: safeGroupJid,
|
|
154
|
+
previousSessionId: safePreviousSessionId,
|
|
155
|
+
newSessionId: safeNewSessionId,
|
|
156
|
+
changeReason: reason,
|
|
157
|
+
changedBy,
|
|
158
|
+
assignmentVersion: safeVersion,
|
|
159
|
+
metadata,
|
|
160
|
+
},
|
|
161
|
+
connection,
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const getOwner = async (groupJid, { bypassCache = false } = {}) => {
|
|
166
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
167
|
+
if (!safeGroupJid) return null;
|
|
168
|
+
|
|
169
|
+
const nowMs = nowImpl();
|
|
170
|
+
if (!bypassCache) {
|
|
171
|
+
const cached = getCacheEntry(safeGroupJid, nowMs);
|
|
172
|
+
if (cached) return cloneOwnerState(cached.owner);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const assignment = await repository.getAssignment(safeGroupJid);
|
|
176
|
+
const ownerState = toOwnerState(assignment, nowMs);
|
|
177
|
+
setCacheEntry(safeGroupJid, ownerState, nowMs);
|
|
178
|
+
return cloneOwnerState(ownerState);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const listAssignments = async ({ groupJid = null, ownerSessionId = null, includeExpired = false, limit = 200 } = {}) => {
|
|
182
|
+
const assignments = await repository.listAssignments({
|
|
183
|
+
groupJid,
|
|
184
|
+
ownerSessionId,
|
|
185
|
+
includeExpired,
|
|
186
|
+
limit,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const nowMs = nowImpl();
|
|
190
|
+
return (Array.isArray(assignments) ? assignments : []).map((assignment) => {
|
|
191
|
+
const owner = toOwnerState(assignment, nowMs);
|
|
192
|
+
if (owner) return owner;
|
|
193
|
+
return {
|
|
194
|
+
groupJid: assignment?.groupJid || null,
|
|
195
|
+
ownerSessionId: assignment?.ownerSessionId || null,
|
|
196
|
+
leaseExpiresAt: cloneDate(assignment?.leaseExpiresAt),
|
|
197
|
+
cooldownUntil: cloneDate(assignment?.cooldownUntil),
|
|
198
|
+
assignmentVersion: Number(assignment?.assignmentVersion || 1),
|
|
199
|
+
pinned: assignment?.pinned === true,
|
|
200
|
+
lastReason: assignment?.lastReason || null,
|
|
201
|
+
createdAt: cloneDate(assignment?.createdAt),
|
|
202
|
+
updatedAt: cloneDate(assignment?.updatedAt),
|
|
203
|
+
active: false,
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const buildFencingToken = ({ groupJid, ownerSessionId, assignmentVersion } = {}) => {
|
|
209
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
210
|
+
const safeOwnerSessionId = repository.normalizeSessionId(ownerSessionId);
|
|
211
|
+
const safeAssignmentVersion = parseAssignmentVersion(assignmentVersion);
|
|
212
|
+
if (!safeGroupJid || !safeOwnerSessionId || !safeAssignmentVersion) return null;
|
|
213
|
+
return `${safeGroupJid}:${safeOwnerSessionId}:${safeAssignmentVersion}`;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const validateFenceToken = async (
|
|
217
|
+
{
|
|
218
|
+
groupJid,
|
|
219
|
+
sessionId,
|
|
220
|
+
assignmentVersion,
|
|
221
|
+
bypassCache = true,
|
|
222
|
+
} = {},
|
|
223
|
+
) => {
|
|
224
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
225
|
+
const safeSessionId = repository.normalizeSessionId(sessionId);
|
|
226
|
+
const safeAssignmentVersion = parseAssignmentVersion(assignmentVersion);
|
|
227
|
+
if (!safeGroupJid || !safeSessionId || !safeAssignmentVersion) {
|
|
228
|
+
return {
|
|
229
|
+
valid: false,
|
|
230
|
+
reason: 'invalid_token',
|
|
231
|
+
owner: null,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const owner = await getOwner(safeGroupJid, { bypassCache });
|
|
236
|
+
if (!owner?.ownerSessionId) {
|
|
237
|
+
return {
|
|
238
|
+
valid: false,
|
|
239
|
+
reason: 'owner_missing',
|
|
240
|
+
owner: null,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (owner.ownerSessionId !== safeSessionId) {
|
|
245
|
+
return {
|
|
246
|
+
valid: false,
|
|
247
|
+
reason: 'owner_mismatch',
|
|
248
|
+
owner: cloneOwnerState(owner),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const currentVersion = parseAssignmentVersion(owner.assignmentVersion);
|
|
253
|
+
if (currentVersion !== safeAssignmentVersion) {
|
|
254
|
+
return {
|
|
255
|
+
valid: false,
|
|
256
|
+
reason: 'assignment_version_mismatch',
|
|
257
|
+
owner: cloneOwnerState(owner),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
valid: true,
|
|
263
|
+
reason: 'ok',
|
|
264
|
+
owner: cloneOwnerState(owner),
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const tryAcquire = async (
|
|
269
|
+
{
|
|
270
|
+
groupJid,
|
|
271
|
+
sessionId,
|
|
272
|
+
leaseMs = safeDefaultLeaseMs,
|
|
273
|
+
reason = 'claim',
|
|
274
|
+
changedBy = null,
|
|
275
|
+
metadata = null,
|
|
276
|
+
} = {},
|
|
277
|
+
) => {
|
|
278
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
279
|
+
const safeSessionId = repository.normalizeSessionId(sessionId);
|
|
280
|
+
if (!safeGroupJid || !safeSessionId) {
|
|
281
|
+
throw new Error('tryAcquire requer groupJid e sessionId validos.');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const safeLeaseMs = resolveLeaseMs(leaseMs);
|
|
285
|
+
const safeChangedBy = repository.normalizeChangedBy(changedBy || safeSessionId || 'system');
|
|
286
|
+
const safeReason = repository.normalizeReason(reason) || 'claim';
|
|
287
|
+
|
|
288
|
+
const outcome = await withTransactionImpl(async (connection) => {
|
|
289
|
+
await sessionRegistry.ensureSession(safeSessionId, { status: 'online', connection });
|
|
290
|
+
|
|
291
|
+
let current = await repository.getAssignmentForUpdate(safeGroupJid, connection);
|
|
292
|
+
const nowMs = nowImpl();
|
|
293
|
+
const leaseExpiresAt = new Date(nowMs + safeLeaseMs);
|
|
294
|
+
|
|
295
|
+
if (!current) {
|
|
296
|
+
try {
|
|
297
|
+
const created = await repository.createAssignment(
|
|
298
|
+
{
|
|
299
|
+
groupJid: safeGroupJid,
|
|
300
|
+
ownerSessionId: safeSessionId,
|
|
301
|
+
leaseExpiresAt,
|
|
302
|
+
reason: safeReason,
|
|
303
|
+
assignmentVersion: 1,
|
|
304
|
+
},
|
|
305
|
+
connection,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const assignmentVersion = Number(created?.assignmentVersion || 1);
|
|
309
|
+
await recordHistory(
|
|
310
|
+
{
|
|
311
|
+
groupJid: safeGroupJid,
|
|
312
|
+
previousSessionId: null,
|
|
313
|
+
newSessionId: safeSessionId,
|
|
314
|
+
reason: safeReason,
|
|
315
|
+
changedBy: safeChangedBy,
|
|
316
|
+
assignmentVersion,
|
|
317
|
+
metadata,
|
|
318
|
+
},
|
|
319
|
+
connection,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
acquired: true,
|
|
324
|
+
owner: toOwnerState(created, nowMs),
|
|
325
|
+
reason: 'created',
|
|
326
|
+
assignmentVersion,
|
|
327
|
+
previousOwnerSessionId: null,
|
|
328
|
+
};
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (!isDuplicateError(error)) {
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
loggerImpl.warn('Conflito de claim concorrente detectado; aplicando fallback transacional.', {
|
|
335
|
+
action: 'group_owner_claim_conflict_fallback',
|
|
336
|
+
groupJid: safeGroupJid,
|
|
337
|
+
sessionId: safeSessionId,
|
|
338
|
+
});
|
|
339
|
+
current = await repository.getAssignmentForUpdate(safeGroupJid, connection);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!current) {
|
|
344
|
+
return {
|
|
345
|
+
acquired: false,
|
|
346
|
+
owner: null,
|
|
347
|
+
reason: 'claim_lost',
|
|
348
|
+
assignmentVersion: null,
|
|
349
|
+
previousOwnerSessionId: null,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const leaseIsActive = hasActiveLease(current, nowMs);
|
|
354
|
+
if (leaseIsActive && current.ownerSessionId !== safeSessionId) {
|
|
355
|
+
return {
|
|
356
|
+
acquired: false,
|
|
357
|
+
owner: toOwnerState(current, nowMs),
|
|
358
|
+
reason: 'owned_by_other',
|
|
359
|
+
assignmentVersion: current.assignmentVersion,
|
|
360
|
+
previousOwnerSessionId: current.ownerSessionId,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (current.ownerSessionId === safeSessionId) {
|
|
365
|
+
const renewed = await repository.updateAssignmentLease(
|
|
366
|
+
{
|
|
367
|
+
groupJid: safeGroupJid,
|
|
368
|
+
ownerSessionId: safeSessionId,
|
|
369
|
+
leaseExpiresAt,
|
|
370
|
+
reason: safeReason,
|
|
371
|
+
},
|
|
372
|
+
connection,
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
acquired: true,
|
|
377
|
+
owner: toOwnerState(renewed, nowMs),
|
|
378
|
+
reason: 'already_owner',
|
|
379
|
+
assignmentVersion: Number(renewed?.assignmentVersion || current.assignmentVersion || 1),
|
|
380
|
+
previousOwnerSessionId: current.ownerSessionId,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const updated = await repository.updateAssignmentOwner(
|
|
385
|
+
{
|
|
386
|
+
groupJid: safeGroupJid,
|
|
387
|
+
ownerSessionId: safeSessionId,
|
|
388
|
+
leaseExpiresAt,
|
|
389
|
+
reason: safeReason,
|
|
390
|
+
bumpVersion: true,
|
|
391
|
+
},
|
|
392
|
+
connection,
|
|
393
|
+
);
|
|
394
|
+
const assignmentVersion = Number(updated?.assignmentVersion || current.assignmentVersion + 1);
|
|
395
|
+
|
|
396
|
+
await recordHistory(
|
|
397
|
+
{
|
|
398
|
+
groupJid: safeGroupJid,
|
|
399
|
+
previousSessionId: current.ownerSessionId,
|
|
400
|
+
newSessionId: safeSessionId,
|
|
401
|
+
reason: safeReason,
|
|
402
|
+
changedBy: safeChangedBy,
|
|
403
|
+
assignmentVersion,
|
|
404
|
+
metadata,
|
|
405
|
+
},
|
|
406
|
+
connection,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
acquired: true,
|
|
411
|
+
owner: toOwnerState(updated, nowMs),
|
|
412
|
+
reason: 'reassigned',
|
|
413
|
+
assignmentVersion,
|
|
414
|
+
previousOwnerSessionId: current.ownerSessionId,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
setCacheEntry(safeGroupJid, outcome.owner ?? null);
|
|
419
|
+
return cloneOutcome(outcome);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const renewLease = async (
|
|
423
|
+
{
|
|
424
|
+
groupJid,
|
|
425
|
+
sessionId,
|
|
426
|
+
leaseMs = safeDefaultLeaseMs,
|
|
427
|
+
reason = 'renew',
|
|
428
|
+
} = {},
|
|
429
|
+
) => {
|
|
430
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
431
|
+
const safeSessionId = repository.normalizeSessionId(sessionId);
|
|
432
|
+
if (!safeGroupJid || !safeSessionId) {
|
|
433
|
+
throw new Error('renewLease requer groupJid e sessionId validos.');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const safeLeaseMs = resolveLeaseMs(leaseMs);
|
|
437
|
+
const safeReason = repository.normalizeReason(reason) || 'renew';
|
|
438
|
+
|
|
439
|
+
const outcome = await withTransactionImpl(async (connection) => {
|
|
440
|
+
const nowMs = nowImpl();
|
|
441
|
+
const current = await repository.getAssignmentForUpdate(safeGroupJid, connection);
|
|
442
|
+
if (!current) {
|
|
443
|
+
return {
|
|
444
|
+
renewed: false,
|
|
445
|
+
owner: null,
|
|
446
|
+
reason: 'not_found',
|
|
447
|
+
assignmentVersion: null,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (current.ownerSessionId !== safeSessionId) {
|
|
452
|
+
return {
|
|
453
|
+
renewed: false,
|
|
454
|
+
owner: toOwnerState(current, nowMs),
|
|
455
|
+
reason: 'not_owner',
|
|
456
|
+
assignmentVersion: Number(current.assignmentVersion || 1),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const leaseExpiresAt = new Date(nowMs + safeLeaseMs);
|
|
461
|
+
const updated = await repository.updateAssignmentLease(
|
|
462
|
+
{
|
|
463
|
+
groupJid: safeGroupJid,
|
|
464
|
+
ownerSessionId: safeSessionId,
|
|
465
|
+
leaseExpiresAt,
|
|
466
|
+
reason: safeReason,
|
|
467
|
+
},
|
|
468
|
+
connection,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
renewed: true,
|
|
473
|
+
owner: toOwnerState(updated, nowMs),
|
|
474
|
+
reason: 'renewed',
|
|
475
|
+
assignmentVersion: Number(updated?.assignmentVersion || current.assignmentVersion || 1),
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
setCacheEntry(safeGroupJid, outcome.owner ?? null);
|
|
480
|
+
return cloneOutcome(outcome);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const heartbeatOwnerSession = async (
|
|
484
|
+
{
|
|
485
|
+
sessionId,
|
|
486
|
+
leaseMs = safeDefaultLeaseMs,
|
|
487
|
+
reason = 'heartbeat',
|
|
488
|
+
botJid = undefined,
|
|
489
|
+
metadata = undefined,
|
|
490
|
+
currentScore = 0,
|
|
491
|
+
capacityWeight = 1,
|
|
492
|
+
} = {},
|
|
493
|
+
) => {
|
|
494
|
+
const safeSessionId = repository.normalizeSessionId(sessionId);
|
|
495
|
+
if (!safeSessionId) {
|
|
496
|
+
throw new Error('heartbeatOwnerSession requer sessionId valido.');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const safeLeaseMs = resolveLeaseMs(leaseMs);
|
|
500
|
+
const safeReason = repository.normalizeReason(reason) || 'heartbeat';
|
|
501
|
+
const heartbeatAt = new Date(nowImpl());
|
|
502
|
+
const leaseExpiresAt = new Date(heartbeatAt.getTime() + safeLeaseMs);
|
|
503
|
+
|
|
504
|
+
const renewedAssignments = await withTransactionImpl(async (connection) => {
|
|
505
|
+
await sessionRegistry.heartbeatSession(safeSessionId, {
|
|
506
|
+
status: 'online',
|
|
507
|
+
currentScore,
|
|
508
|
+
metadata,
|
|
509
|
+
botJid,
|
|
510
|
+
capacityWeight,
|
|
511
|
+
connection,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
return repository.renewLeasesByOwner(
|
|
515
|
+
{
|
|
516
|
+
ownerSessionId: safeSessionId,
|
|
517
|
+
leaseExpiresAt,
|
|
518
|
+
reason: safeReason,
|
|
519
|
+
now: heartbeatAt,
|
|
520
|
+
},
|
|
521
|
+
connection,
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
renewedAssignments: Number(renewedAssignments || 0),
|
|
527
|
+
heartbeatAt: new Date(heartbeatAt.getTime()),
|
|
528
|
+
leaseExpiresAt: new Date(leaseExpiresAt.getTime()),
|
|
529
|
+
sessionId: safeSessionId,
|
|
530
|
+
reason: safeReason,
|
|
531
|
+
};
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const release = async (
|
|
535
|
+
{
|
|
536
|
+
groupJid,
|
|
537
|
+
sessionId = null,
|
|
538
|
+
reason = 'release',
|
|
539
|
+
changedBy = null,
|
|
540
|
+
metadata = null,
|
|
541
|
+
} = {},
|
|
542
|
+
) => {
|
|
543
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
544
|
+
const safeSessionId = repository.normalizeSessionId(sessionId);
|
|
545
|
+
if (!safeGroupJid) {
|
|
546
|
+
throw new Error('release requer groupJid valido.');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const safeReason = repository.normalizeReason(reason) || 'release';
|
|
550
|
+
const safeChangedBy = repository.normalizeChangedBy(changedBy || safeSessionId || 'system');
|
|
551
|
+
|
|
552
|
+
const outcome = await withTransactionImpl(async (connection) => {
|
|
553
|
+
const nowMs = nowImpl();
|
|
554
|
+
const current = await repository.getAssignmentForUpdate(safeGroupJid, connection);
|
|
555
|
+
if (!current) {
|
|
556
|
+
return {
|
|
557
|
+
released: false,
|
|
558
|
+
owner: null,
|
|
559
|
+
reason: 'not_found',
|
|
560
|
+
assignmentVersion: null,
|
|
561
|
+
previousOwnerSessionId: null,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const leaseIsActive = hasActiveLease(current, nowMs);
|
|
566
|
+
if (safeSessionId && current.ownerSessionId !== safeSessionId && leaseIsActive) {
|
|
567
|
+
return {
|
|
568
|
+
released: false,
|
|
569
|
+
owner: toOwnerState(current, nowMs),
|
|
570
|
+
reason: 'not_owner',
|
|
571
|
+
assignmentVersion: Number(current.assignmentVersion || 1),
|
|
572
|
+
previousOwnerSessionId: current.ownerSessionId,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const ownerFilter = safeSessionId && current.ownerSessionId === safeSessionId ? safeSessionId : null;
|
|
577
|
+
|
|
578
|
+
const expired = await repository.expireAssignment(
|
|
579
|
+
{
|
|
580
|
+
groupJid: safeGroupJid,
|
|
581
|
+
ownerSessionId: ownerFilter,
|
|
582
|
+
reason: safeReason,
|
|
583
|
+
bumpVersion: leaseIsActive,
|
|
584
|
+
leaseExpiresAt: new Date(nowMs),
|
|
585
|
+
},
|
|
586
|
+
connection,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const assignmentVersion = Number(expired?.assignmentVersion || (leaseIsActive ? Number(current.assignmentVersion || 1) + 1 : Number(current.assignmentVersion || 1)));
|
|
590
|
+
|
|
591
|
+
if (leaseIsActive && current.ownerSessionId) {
|
|
592
|
+
await recordHistory(
|
|
593
|
+
{
|
|
594
|
+
groupJid: safeGroupJid,
|
|
595
|
+
previousSessionId: current.ownerSessionId,
|
|
596
|
+
newSessionId: current.ownerSessionId,
|
|
597
|
+
reason: safeReason,
|
|
598
|
+
changedBy: safeChangedBy,
|
|
599
|
+
assignmentVersion,
|
|
600
|
+
metadata,
|
|
601
|
+
},
|
|
602
|
+
connection,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
released: true,
|
|
608
|
+
owner: null,
|
|
609
|
+
reason: 'released',
|
|
610
|
+
assignmentVersion,
|
|
611
|
+
previousOwnerSessionId: current.ownerSessionId,
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
if (outcome.released) {
|
|
616
|
+
setCacheEntry(safeGroupJid, null);
|
|
617
|
+
} else {
|
|
618
|
+
setCacheEntry(safeGroupJid, outcome.owner ?? null);
|
|
619
|
+
}
|
|
620
|
+
return cloneOutcome(outcome);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const forceAssign = async (
|
|
624
|
+
{
|
|
625
|
+
groupJid,
|
|
626
|
+
sessionId,
|
|
627
|
+
leaseMs = safeDefaultLeaseMs,
|
|
628
|
+
reason = 'force_assign',
|
|
629
|
+
changedBy = null,
|
|
630
|
+
metadata = null,
|
|
631
|
+
pinned = undefined,
|
|
632
|
+
} = {},
|
|
633
|
+
) => {
|
|
634
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
635
|
+
const safeSessionId = repository.normalizeSessionId(sessionId);
|
|
636
|
+
if (!safeGroupJid || !safeSessionId) {
|
|
637
|
+
throw new Error('forceAssign requer groupJid e sessionId validos.');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const safeLeaseMs = resolveLeaseMs(leaseMs);
|
|
641
|
+
const safeReason = repository.normalizeReason(reason) || 'force_assign';
|
|
642
|
+
const safeChangedBy = repository.normalizeChangedBy(changedBy || safeSessionId || 'system');
|
|
643
|
+
|
|
644
|
+
const outcome = await withTransactionImpl(async (connection) => {
|
|
645
|
+
await sessionRegistry.ensureSession(safeSessionId, { status: 'online', connection });
|
|
646
|
+
|
|
647
|
+
const nowMs = nowImpl();
|
|
648
|
+
const leaseExpiresAt = new Date(nowMs + safeLeaseMs);
|
|
649
|
+
const current = await repository.getAssignmentForUpdate(safeGroupJid, connection);
|
|
650
|
+
|
|
651
|
+
if (!current) {
|
|
652
|
+
const created = await repository.createAssignment(
|
|
653
|
+
{
|
|
654
|
+
groupJid: safeGroupJid,
|
|
655
|
+
ownerSessionId: safeSessionId,
|
|
656
|
+
leaseExpiresAt,
|
|
657
|
+
reason: safeReason,
|
|
658
|
+
pinned: pinned === undefined ? false : pinned === true,
|
|
659
|
+
assignmentVersion: 1,
|
|
660
|
+
},
|
|
661
|
+
connection,
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
const assignmentVersion = Number(created?.assignmentVersion || 1);
|
|
665
|
+
await recordHistory(
|
|
666
|
+
{
|
|
667
|
+
groupJid: safeGroupJid,
|
|
668
|
+
previousSessionId: null,
|
|
669
|
+
newSessionId: safeSessionId,
|
|
670
|
+
reason: safeReason,
|
|
671
|
+
changedBy: safeChangedBy,
|
|
672
|
+
assignmentVersion,
|
|
673
|
+
metadata,
|
|
674
|
+
},
|
|
675
|
+
connection,
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
reassigned: true,
|
|
680
|
+
owner: toOwnerState(created, nowMs),
|
|
681
|
+
reason: 'created',
|
|
682
|
+
assignmentVersion,
|
|
683
|
+
previousOwnerSessionId: null,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const isOwnerChanged = current.ownerSessionId !== safeSessionId;
|
|
688
|
+
const hasLeaseActive = hasActiveLease(current, nowMs);
|
|
689
|
+
const nextLeaseExpiresAt = hasLeaseActive ? leaseExpiresAt : new Date(nowMs + safeLeaseMs);
|
|
690
|
+
const updated = await repository.updateAssignmentOwner(
|
|
691
|
+
{
|
|
692
|
+
groupJid: safeGroupJid,
|
|
693
|
+
ownerSessionId: safeSessionId,
|
|
694
|
+
leaseExpiresAt: nextLeaseExpiresAt,
|
|
695
|
+
reason: safeReason,
|
|
696
|
+
bumpVersion: isOwnerChanged,
|
|
697
|
+
pinned,
|
|
698
|
+
},
|
|
699
|
+
connection,
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
const assignmentVersion = Number(updated?.assignmentVersion || (isOwnerChanged ? Number(current.assignmentVersion || 1) + 1 : Number(current.assignmentVersion || 1)));
|
|
703
|
+
|
|
704
|
+
if (isOwnerChanged) {
|
|
705
|
+
await recordHistory(
|
|
706
|
+
{
|
|
707
|
+
groupJid: safeGroupJid,
|
|
708
|
+
previousSessionId: current.ownerSessionId,
|
|
709
|
+
newSessionId: safeSessionId,
|
|
710
|
+
reason: safeReason,
|
|
711
|
+
changedBy: safeChangedBy,
|
|
712
|
+
assignmentVersion,
|
|
713
|
+
metadata,
|
|
714
|
+
},
|
|
715
|
+
connection,
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
reassigned: isOwnerChanged,
|
|
721
|
+
owner: toOwnerState(updated, nowMs),
|
|
722
|
+
reason: isOwnerChanged ? 'reassigned' : 'already_owner',
|
|
723
|
+
assignmentVersion,
|
|
724
|
+
previousOwnerSessionId: current.ownerSessionId,
|
|
725
|
+
};
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
setCacheEntry(safeGroupJid, outcome.owner ?? null);
|
|
729
|
+
return cloneOutcome(outcome);
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const setPinned = async (
|
|
733
|
+
{
|
|
734
|
+
groupJid,
|
|
735
|
+
pinned,
|
|
736
|
+
sessionId = null,
|
|
737
|
+
reason = null,
|
|
738
|
+
changedBy = null,
|
|
739
|
+
metadata = null,
|
|
740
|
+
leaseMs = safeDefaultLeaseMs,
|
|
741
|
+
} = {},
|
|
742
|
+
) => {
|
|
743
|
+
const safeGroupJid = repository.normalizeGroupJid(groupJid);
|
|
744
|
+
if (!safeGroupJid) {
|
|
745
|
+
throw new Error('setPinned requer groupJid valido.');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const desiredPinned = pinned === true;
|
|
749
|
+
const safeSessionId = repository.normalizeSessionId(sessionId);
|
|
750
|
+
const safeReason = repository.normalizeReason(reason) || (desiredPinned ? 'pin_assignment' : 'unpin_assignment');
|
|
751
|
+
const safeChangedBy = repository.normalizeChangedBy(changedBy || safeSessionId || 'system');
|
|
752
|
+
const safeLeaseMs = resolveLeaseMs(leaseMs);
|
|
753
|
+
|
|
754
|
+
const outcome = await withTransactionImpl(async (connection) => {
|
|
755
|
+
const nowMs = nowImpl();
|
|
756
|
+
const current = await repository.getAssignmentForUpdate(safeGroupJid, connection);
|
|
757
|
+
if (!current) {
|
|
758
|
+
if (!desiredPinned) {
|
|
759
|
+
return {
|
|
760
|
+
updated: false,
|
|
761
|
+
owner: null,
|
|
762
|
+
reason: 'not_found',
|
|
763
|
+
assignmentVersion: null,
|
|
764
|
+
previousOwnerSessionId: null,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (!safeSessionId) {
|
|
769
|
+
throw new Error('Nao e possivel pinar grupo sem assignment existente sem informar sessionId.');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
await sessionRegistry.ensureSession(safeSessionId, { status: 'online', connection });
|
|
773
|
+
const created = await repository.createAssignment(
|
|
774
|
+
{
|
|
775
|
+
groupJid: safeGroupJid,
|
|
776
|
+
ownerSessionId: safeSessionId,
|
|
777
|
+
leaseExpiresAt: new Date(nowMs + safeLeaseMs),
|
|
778
|
+
reason: safeReason,
|
|
779
|
+
pinned: true,
|
|
780
|
+
assignmentVersion: 1,
|
|
781
|
+
},
|
|
782
|
+
connection,
|
|
783
|
+
);
|
|
784
|
+
const assignmentVersion = Number(created?.assignmentVersion || 1);
|
|
785
|
+
await recordHistory(
|
|
786
|
+
{
|
|
787
|
+
groupJid: safeGroupJid,
|
|
788
|
+
previousSessionId: null,
|
|
789
|
+
newSessionId: safeSessionId,
|
|
790
|
+
reason: safeReason,
|
|
791
|
+
changedBy: safeChangedBy,
|
|
792
|
+
assignmentVersion,
|
|
793
|
+
metadata,
|
|
794
|
+
},
|
|
795
|
+
connection,
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
updated: true,
|
|
800
|
+
owner: toOwnerState(created, nowMs),
|
|
801
|
+
reason: 'created_and_pinned',
|
|
802
|
+
assignmentVersion,
|
|
803
|
+
previousOwnerSessionId: null,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const targetSessionId = safeSessionId || current.ownerSessionId;
|
|
808
|
+
const hasLease = hasActiveLease(current, nowMs);
|
|
809
|
+
const leaseExpiresAt = hasLease ? current.leaseExpiresAt : new Date(nowMs + safeLeaseMs);
|
|
810
|
+
|
|
811
|
+
const updated = await repository.updateAssignmentOwner(
|
|
812
|
+
{
|
|
813
|
+
groupJid: safeGroupJid,
|
|
814
|
+
ownerSessionId: targetSessionId,
|
|
815
|
+
leaseExpiresAt,
|
|
816
|
+
reason: safeReason,
|
|
817
|
+
bumpVersion: targetSessionId !== current.ownerSessionId,
|
|
818
|
+
pinned: desiredPinned,
|
|
819
|
+
},
|
|
820
|
+
connection,
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
const assignmentVersion = Number(updated?.assignmentVersion || current.assignmentVersion || 1);
|
|
824
|
+
const ownerChanged = targetSessionId !== current.ownerSessionId;
|
|
825
|
+
if (ownerChanged) {
|
|
826
|
+
await recordHistory(
|
|
827
|
+
{
|
|
828
|
+
groupJid: safeGroupJid,
|
|
829
|
+
previousSessionId: current.ownerSessionId,
|
|
830
|
+
newSessionId: targetSessionId,
|
|
831
|
+
reason: safeReason,
|
|
832
|
+
changedBy: safeChangedBy,
|
|
833
|
+
assignmentVersion,
|
|
834
|
+
metadata,
|
|
835
|
+
},
|
|
836
|
+
connection,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return {
|
|
841
|
+
updated: true,
|
|
842
|
+
owner: toOwnerState(updated, nowMs),
|
|
843
|
+
reason: ownerChanged ? 'owner_changed_and_pin_updated' : 'pin_updated',
|
|
844
|
+
assignmentVersion,
|
|
845
|
+
previousOwnerSessionId: current.ownerSessionId,
|
|
846
|
+
};
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
setCacheEntry(safeGroupJid, outcome.owner ?? null);
|
|
850
|
+
return cloneOutcome(outcome);
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const getCacheStats = () => ({
|
|
854
|
+
size: ownerCache.size,
|
|
855
|
+
ttlMs: safeCacheTtlMs,
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
return {
|
|
859
|
+
getOwner,
|
|
860
|
+
listAssignments,
|
|
861
|
+
tryAcquire,
|
|
862
|
+
renewLease,
|
|
863
|
+
heartbeatOwnerSession,
|
|
864
|
+
release,
|
|
865
|
+
forceAssign,
|
|
866
|
+
setPinned,
|
|
867
|
+
recordHistory,
|
|
868
|
+
validateFenceToken,
|
|
869
|
+
buildFencingToken,
|
|
870
|
+
invalidateCache,
|
|
871
|
+
clearCache,
|
|
872
|
+
getCacheStats,
|
|
873
|
+
};
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const groupOwnershipService = createGroupOwnershipService();
|
|
877
|
+
|
|
878
|
+
export const getOwner = (...args) => groupOwnershipService.getOwner(...args);
|
|
879
|
+
export const listAssignments = (...args) => groupOwnershipService.listAssignments(...args);
|
|
880
|
+
export const tryAcquire = (...args) => groupOwnershipService.tryAcquire(...args);
|
|
881
|
+
export const renewLease = (...args) => groupOwnershipService.renewLease(...args);
|
|
882
|
+
export const heartbeatOwnerSession = (...args) => groupOwnershipService.heartbeatOwnerSession(...args);
|
|
883
|
+
export const release = (...args) => groupOwnershipService.release(...args);
|
|
884
|
+
export const forceAssign = (...args) => groupOwnershipService.forceAssign(...args);
|
|
885
|
+
export const setPinned = (...args) => groupOwnershipService.setPinned(...args);
|
|
886
|
+
export const recordHistory = (...args) => groupOwnershipService.recordHistory(...args);
|
|
887
|
+
export const validateFenceToken = (...args) => groupOwnershipService.validateFenceToken(...args);
|
|
888
|
+
export const buildFencingToken = (...args) => groupOwnershipService.buildFencingToken(...args);
|
|
889
|
+
|
|
890
|
+
export default groupOwnershipService;
|