@omnizap-system/omnizap 2.6.1 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +78 -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 +6 -0
- package/app/configParts/adminIdentity.js +36 -7
- package/app/configParts/baileysConfig.js +343 -56
- package/app/configParts/groupUtils.js +226 -0
- package/app/configParts/loggerConfig.js +185 -0
- package/app/configParts/messagePersistenceService.js +307 -5
- package/app/configParts/sessionConfig.js +242 -0
- package/app/connection/baileysCompatibility.test.js +10 -1
- package/app/connection/baileysDbAuthState.js +205 -9
- package/app/connection/baileysLibsignalPatch.js +210 -0
- package/app/connection/groupOwnerWriteStateResolver.js +141 -0
- package/app/connection/socketController.js +694 -123
- package/app/connection/socketController.multiSession.test.js +128 -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 +96 -4
- package/app/controllers/messageProcessingPipeline.js +90 -9
- package/app/controllers/messageProcessingPipeline.test.js +202 -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 +452 -0
- package/app/services/multiSession/groupOwnershipRepository.js +346 -0
- package/app/services/multiSession/groupOwnershipService.js +809 -0
- package/app/services/multiSession/groupOwnershipService.test.js +317 -0
- package/app/services/multiSession/sessionRegistryService.js +239 -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 +391 -25
- 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 +14 -6
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/css/payments-react.css +478 -0
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/homeReactApp.js +2 -2
- package/public/js/apps/paymentsCancelReactApp.js +45 -0
- package/public/js/apps/paymentsReactApp.js +399 -0
- package/public/js/apps/paymentsSuccessReactApp.js +148 -0
- 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/pagamentos-cancelado.html +21 -0
- package/public/pages/pagamentos-sucesso.html +21 -0
- package/public/pages/pagamentos.html +30 -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 +13 -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 +564 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +254 -0
- package/server/controllers/payments/paymentsController.js +731 -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 +228 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailAutomationRuntime.js +36 -1
- package/server/email/emailAutomationService.js +42 -1
- package/server/email/emailTemplateService.js +140 -33
- package/server/http/httpRequestUtils.js +18 -14
- package/server/http/httpServer.js +8 -4
- package/server/middleware/securityHeaders.js +35 -3
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +50 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/payments/paymentsRouter.js +47 -0
- package/server/routes/static/staticPageRouter.js +30 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +5 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -67,6 +67,7 @@ export const stripWebpExtension = (value) =>
|
|
|
67
67
|
.replace(/\.webp$/i, '');
|
|
68
68
|
|
|
69
69
|
const clampInt = (value, fallback, min, max) => {
|
|
70
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
70
71
|
const parsed = Number(value);
|
|
71
72
|
if (!Number.isFinite(parsed)) return fallback;
|
|
72
73
|
return Math.max(min, Math.min(max, Math.floor(parsed)));
|
|
@@ -964,8 +965,6 @@ const convertUploadMediaToWebp = async ({ ownerJid, buffer, mimetype }) => {
|
|
|
964
965
|
const PACK_TAG_MARKER_REGEX = /\[pack-tags:([^\]]+)\]/i;
|
|
965
966
|
const AUTO_PACK_MARKER_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/gi;
|
|
966
967
|
const AUTO_PACK_MARKER_TEST_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/i;
|
|
967
|
-
const AUTO_PACK_COLLECTOR_MARKER = '[auto-pack:collector]';
|
|
968
|
-
const AUTO_PACK_COLLECTOR_LEGACY_TEXT = 'coleção automática de figurinhas criadas pelo usuário.';
|
|
969
968
|
const AUTO_PACK_DESCRIPTION_PREFIX_REGEX = /^curadoria automática por tema\.\s*tema:\s*[^.]+\.?\s*(?:score\s*=\s*-?\d+(?:\.\d+)?\.?\s*)?/i;
|
|
970
969
|
const AUTO_PACK_SCORE_FRAGMENT_REGEX = /\bscore\s*=\s*-?\d+(?:\.\d+)?\.?/gi;
|
|
971
970
|
const normalizePackTag = (value) =>
|
|
@@ -1024,29 +1023,10 @@ const parsePackDescriptionMetadata = (description) => {
|
|
|
1024
1023
|
};
|
|
1025
1024
|
};
|
|
1026
1025
|
|
|
1027
|
-
const isCollectorAutoPack = (pack) => {
|
|
1028
|
-
if (!pack || typeof pack !== 'object') return false;
|
|
1029
|
-
const description = String(pack.description || '').toLowerCase();
|
|
1030
|
-
return description.includes(AUTO_PACK_COLLECTOR_MARKER) || description.includes(AUTO_PACK_COLLECTOR_LEGACY_TEXT);
|
|
1031
|
-
};
|
|
1032
|
-
|
|
1033
|
-
const isThemeCurationAutoPack = (pack) => {
|
|
1034
|
-
if (!pack || typeof pack !== 'object') return false;
|
|
1035
|
-
const name = String(pack.name || '').trim();
|
|
1036
|
-
if (/^\[auto\]/i.test(name)) return true;
|
|
1037
|
-
|
|
1038
|
-
const description = String(pack.description || '').toLowerCase();
|
|
1039
|
-
if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
|
|
1040
|
-
|
|
1041
|
-
return Boolean(String(pack.pack_theme_key || '').trim());
|
|
1042
|
-
};
|
|
1043
|
-
|
|
1044
1026
|
const shouldHidePackFromMyProfileDefault = (pack, { includeAutoPacks = false } = {}) => {
|
|
1045
1027
|
if (!pack || typeof pack !== 'object') return false;
|
|
1046
1028
|
if (includeAutoPacks) return false;
|
|
1047
|
-
|
|
1048
|
-
if (isThemeCurationAutoPack(pack)) return true;
|
|
1049
|
-
return pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1;
|
|
1029
|
+
return false;
|
|
1050
1030
|
};
|
|
1051
1031
|
|
|
1052
1032
|
const buildPackDescriptionWithTags = (description, tags = []) => {
|
|
@@ -1734,6 +1714,12 @@ const getMarketplaceStatsCached = async (visibility) => {
|
|
|
1734
1714
|
bucket.expiresAt = __timeNowMs() + HOME_MARKETPLACE_STATS_CACHE_SECONDS * 1000;
|
|
1735
1715
|
return data;
|
|
1736
1716
|
})
|
|
1717
|
+
.catch((error) => {
|
|
1718
|
+
if (hasValue && bucket.value) {
|
|
1719
|
+
return bucket.value;
|
|
1720
|
+
}
|
|
1721
|
+
throw error;
|
|
1722
|
+
})
|
|
1737
1723
|
.finally(() => {
|
|
1738
1724
|
bucket.pending = null;
|
|
1739
1725
|
});
|
|
@@ -3219,7 +3205,7 @@ const handleCreatePackRequest = async (req, res) => {
|
|
|
3219
3205
|
});
|
|
3220
3206
|
const manualTags = mergeUniqueTags(Array.isArray(payload?.tags) ? payload.tags : []).slice(0, 8);
|
|
3221
3207
|
const persistedDescription = buildPackDescriptionWithTags(description, manualTags);
|
|
3222
|
-
const visibility = String(payload?.visibility || '
|
|
3208
|
+
const visibility = String(payload?.visibility || 'private')
|
|
3223
3209
|
.trim()
|
|
3224
3210
|
.toLowerCase();
|
|
3225
3211
|
const googleSession = await resolveGoogleWebSessionFromRequest(req);
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import logger from '#logger';
|
|
2
2
|
import { getActiveSocket, getAdminPhone, getAdminRawValue, getJidUser, resolveAdminJid, resolveBotJid, extractUserIdInfo, resolveUserId } from '../../../app/config/index.js';
|
|
3
|
+
import { isLikelyWhatsAppPhone, normalizePhoneDigits, resolveAdminPhoneFromEnv, resolveBotPhoneFromEnv, resolveSupportPhoneFromEnv } from '../../../utils/whatsapp/contactEnv.js';
|
|
3
4
|
|
|
4
5
|
const PACK_COMMAND_PREFIX = String(process.env.COMMAND_PREFIX || '/').trim() || '/';
|
|
5
6
|
|
|
6
|
-
const normalizePhoneDigits = (value) => String(value || '').replace(/\D+/g, '');
|
|
7
|
-
|
|
8
7
|
const isPlausibleWhatsAppPhone = (value) => {
|
|
9
8
|
const digits = normalizePhoneDigits(value);
|
|
10
|
-
return digits
|
|
9
|
+
return isLikelyWhatsAppPhone(digits) ? digits : '';
|
|
11
10
|
};
|
|
12
11
|
|
|
13
12
|
const resolveActiveSocketBotJid = (activeSocket) => {
|
|
@@ -26,18 +25,12 @@ export const resolveCatalogBotPhone = () => {
|
|
|
26
25
|
const jidUser = botJid ? getJidUser(botJid) : null;
|
|
27
26
|
const fromSocket = normalizePhoneDigits(jidUser || '');
|
|
28
27
|
|
|
29
|
-
if (fromSocket
|
|
28
|
+
if (isLikelyWhatsAppPhone(fromSocket)) {
|
|
30
29
|
return fromSocket;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
for (const candidate of envCandidates) {
|
|
36
|
-
const digits = normalizePhoneDigits(candidate || '');
|
|
37
|
-
if (digits && digits.length >= 10 && digits.length <= 15) {
|
|
38
|
-
return digits;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
32
|
+
const fromEnv = resolveBotPhoneFromEnv({ fallback: '' });
|
|
33
|
+
if (fromEnv) return fromEnv;
|
|
41
34
|
|
|
42
35
|
logger.warn('Nao foi possivel resolver o numero do bot para contato.', {
|
|
43
36
|
action: 'resolve_bot_phone_failed',
|
|
@@ -75,12 +68,11 @@ const resolveSupportAdminPhone = async () => {
|
|
|
75
68
|
const adminPhone = isPlausibleWhatsAppPhone(getAdminPhone() || '');
|
|
76
69
|
if (adminPhone) return adminPhone;
|
|
77
70
|
|
|
78
|
-
const
|
|
71
|
+
const configuredAdminPhone = resolveAdminPhoneFromEnv({ fallback: '' });
|
|
72
|
+
if (configuredAdminPhone) return configuredAdminPhone;
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (digits) return digits;
|
|
83
|
-
}
|
|
74
|
+
const configuredSupportPhone = resolveSupportPhoneFromEnv({ fallback: '' });
|
|
75
|
+
if (configuredSupportPhone) return configuredSupportPhone;
|
|
84
76
|
|
|
85
77
|
return '';
|
|
86
78
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
|
|
2
2
|
import { withTimeout } from '../../http/httpRequestUtils.js';
|
|
3
|
+
import { resolveBotPhoneFromEnv } from '../../../utils/whatsapp/contactEnv.js';
|
|
3
4
|
|
|
4
5
|
export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger, getSystemMetrics, getActiveSocket, resolveSocketReadyState, resolveActiveSocketBotJid, resolveCatalogBotPhone, fetchPrometheusSummary, metricsEndpoint, systemSummaryCache, systemSummaryCacheSeconds, readmeSummaryCache, readmeSummaryCacheSeconds, readmeMessageTypeSampleLimit, readmeCommandPrefix, buildMenuCaption, buildStickerMenu, buildMediaMenu, buildQuoteMenu, buildAnimeMenu, buildAiMenu, buildStatsMenu, buildAdminMenu, profilePictureUrlFromActiveSocket, normalizeJid, getJidUser, globalRankCache, globalRankRefreshSeconds, marketplaceGlobalStatsCache, marketplaceGlobalStatsCacheSeconds }) => {
|
|
5
6
|
let globalRankRefreshTimer = null;
|
|
@@ -153,6 +154,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
|
|
|
153
154
|
systemSummaryCache.expiresAt = __timeNowMs() + systemSummaryCacheSeconds * 1000;
|
|
154
155
|
return payload;
|
|
155
156
|
})
|
|
157
|
+
.catch((error) => {
|
|
158
|
+
if (hasValue && systemSummaryCache.value) {
|
|
159
|
+
return systemSummaryCache.value;
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
})
|
|
156
163
|
.finally(() => {
|
|
157
164
|
systemSummaryCache.pending = null;
|
|
158
165
|
});
|
|
@@ -329,6 +336,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
|
|
|
329
336
|
readmeSummaryCache.expiresAt = __timeNowMs() + readmeSummaryCacheSeconds * 1000;
|
|
330
337
|
return payload;
|
|
331
338
|
})
|
|
339
|
+
.catch((error) => {
|
|
340
|
+
if (hasValue && readmeSummaryCache.value) {
|
|
341
|
+
return readmeSummaryCache.value;
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
})
|
|
332
345
|
.finally(() => {
|
|
333
346
|
readmeSummaryCache.pending = null;
|
|
334
347
|
});
|
|
@@ -346,12 +359,8 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
|
|
|
346
359
|
const botPhoneFromCatalog = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '');
|
|
347
360
|
if (botPhoneFromCatalog) candidates.add(botPhoneFromCatalog);
|
|
348
361
|
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
for (const candidate of envCandidates) {
|
|
352
|
-
const digits = String(candidate || '').replace(/\D+/g, '');
|
|
353
|
-
if (digits) candidates.add(digits);
|
|
354
|
-
}
|
|
362
|
+
const configuredBotPhone = String(resolveBotPhoneFromEnv({ fallback: '' }) || '').replace(/\D+/g, '');
|
|
363
|
+
if (configuredBotPhone) candidates.add(configuredBotPhone);
|
|
355
364
|
|
|
356
365
|
return Array.from(candidates).filter((value) => value.length >= 8);
|
|
357
366
|
};
|
|
@@ -542,6 +551,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
|
|
|
542
551
|
globalRankCache.expiresAt = __timeNowMs() + globalRankRefreshSeconds * 1000;
|
|
543
552
|
return data;
|
|
544
553
|
})
|
|
554
|
+
.catch((error) => {
|
|
555
|
+
if (hasValue && globalRankCache.value) {
|
|
556
|
+
return globalRankCache.value;
|
|
557
|
+
}
|
|
558
|
+
throw error;
|
|
559
|
+
})
|
|
545
560
|
.finally(() => {
|
|
546
561
|
globalRankCache.pending = null;
|
|
547
562
|
});
|
|
@@ -737,6 +752,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
|
|
|
737
752
|
marketplaceGlobalStatsCache.expiresAt = __timeNowMs() + marketplaceGlobalStatsCacheSeconds * 1000;
|
|
738
753
|
return data;
|
|
739
754
|
})
|
|
755
|
+
.catch((error) => {
|
|
756
|
+
if (hasValue && marketplaceGlobalStatsCache.value) {
|
|
757
|
+
return marketplaceGlobalStatsCache.value;
|
|
758
|
+
}
|
|
759
|
+
throw error;
|
|
760
|
+
})
|
|
740
761
|
.finally(() => {
|
|
741
762
|
marketplaceGlobalStatsCache.pending = null;
|
|
742
763
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logger from '#logger';
|
|
2
2
|
import { executeQuery, TABLES } from '../../../database/index.js';
|
|
3
|
-
import { getActiveSocket, getJidUser, normalizeJid, profilePictureUrlFromActiveSocket } from '../../../app/config/index.js';
|
|
3
|
+
import { getActiveSocket, getActiveSocketsBySession, getJidUser, getMultiSessionRuntimeConfig, isSocketOpen, normalizeJid, profilePictureUrlFromActiveSocket } from '../../../app/config/index.js';
|
|
4
4
|
import { getSystemMetrics } from '../../../app/utils/systemMetrics/systemMetricsModule.js';
|
|
5
5
|
import { createStickerCatalogSystemContext } from './stickerCatalogSystemContext.js';
|
|
6
6
|
import { createStickerCatalogNonCatalogHandlers } from '../sticker/nonCatalogHandlers.js';
|
|
@@ -10,6 +10,9 @@ import { fetchPrometheusSummary } from './systemMetricsController.js';
|
|
|
10
10
|
import { buildBotContactInfo, buildSupportInfo, resolveCatalogBotPhone } from './contactController.js';
|
|
11
11
|
import { buildAdminMenu, buildAiMenu, buildAnimeMenu, buildMediaMenu, buildMenuCaption, buildQuoteMenu, buildStatsMenu, buildStickerMenu } from '../../../app/modules/menuModule/common.js';
|
|
12
12
|
import { trackWebVisitMetric } from './visitController.js';
|
|
13
|
+
import groupOwnershipService from '../../../app/services/multiSession/groupOwnershipService.js';
|
|
14
|
+
import sessionRegistryService from '../../../app/services/multiSession/sessionRegistryService.js';
|
|
15
|
+
import { runGroupAssignmentBalancerCycle } from '../../../app/services/multiSession/assignmentBalancerService.js';
|
|
13
16
|
|
|
14
17
|
const SYSTEM_SUMMARY_CACHE_SECONDS = Number(process.env.SYSTEM_SUMMARY_CACHE_SECONDS || 20);
|
|
15
18
|
const README_SUMMARY_CACHE_SECONDS = Number(process.env.README_SUMMARY_CACHE_SECONDS || 1800);
|
|
@@ -132,4 +135,228 @@ export const systemHandlers = createStickerCatalogNonCatalogHandlers({
|
|
|
132
135
|
isAuthenticatedGoogleSession: (sess) => Boolean(sess?.sub && (sess?.ownerJid || sess?.ownerPhone || sess?.email)),
|
|
133
136
|
});
|
|
134
137
|
|
|
138
|
+
const clampLimit = (value, fallback = 200, min = 1, max = 5_000) => {
|
|
139
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
140
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
141
|
+
return Math.max(min, Math.min(max, parsed));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const normalizeOptional = (value, maxLength = 255) => {
|
|
145
|
+
const normalized = String(value || '')
|
|
146
|
+
.trim()
|
|
147
|
+
.slice(0, maxLength);
|
|
148
|
+
return normalized || null;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const normalizeBoolean = (value, fallback = false) => {
|
|
152
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
153
|
+
const normalized = String(value).trim().toLowerCase();
|
|
154
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
155
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
156
|
+
return fallback;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const toIso = (value) => {
|
|
160
|
+
if (!value) return null;
|
|
161
|
+
const parsed = new Date(value);
|
|
162
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
163
|
+
return parsed.toISOString();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const resolveSocketRuntimeState = (socket) => {
|
|
167
|
+
const open = isSocketOpen(socket);
|
|
168
|
+
const readyState = resolveSocketReadyState(socket);
|
|
169
|
+
const botJid = resolveActiveSocketBotJid(socket);
|
|
170
|
+
return {
|
|
171
|
+
socket_open: open,
|
|
172
|
+
socket_ready_state: readyState,
|
|
173
|
+
socket_bot_jid: botJid || null,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const listSystemAdminSessions = async ({ status = null, limit = 200 } = {}) => {
|
|
178
|
+
const runtimeConfig = getMultiSessionRuntimeConfig();
|
|
179
|
+
const safeStatus = normalizeOptional(status, 24);
|
|
180
|
+
const safeLimit = clampLimit(limit, 200, 1, 5_000);
|
|
181
|
+
const registryRows = await sessionRegistryService.listSessions({
|
|
182
|
+
status: safeStatus,
|
|
183
|
+
limit: safeLimit,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const socketsBySession = getActiveSocketsBySession();
|
|
187
|
+
const knownSessionIds = new Set();
|
|
188
|
+
for (const sessionId of runtimeConfig.sessionIds || []) knownSessionIds.add(sessionId);
|
|
189
|
+
for (const row of registryRows || []) {
|
|
190
|
+
if (row?.sessionId) knownSessionIds.add(row.sessionId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const selectedSessionIds = Array.from(knownSessionIds).filter(Boolean).slice(0, safeLimit);
|
|
194
|
+
const registryBySession = new Map((registryRows || []).map((row) => [row.sessionId, row]));
|
|
195
|
+
|
|
196
|
+
const sessions = selectedSessionIds.map((sessionId) => {
|
|
197
|
+
const row = registryBySession.get(sessionId) || null;
|
|
198
|
+
const socket = socketsBySession.get(sessionId) || null;
|
|
199
|
+
const socketRuntime = resolveSocketRuntimeState(socket);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
session_id: sessionId,
|
|
203
|
+
is_primary: sessionId === runtimeConfig.primarySessionId,
|
|
204
|
+
configured: (runtimeConfig.sessionIds || []).includes(sessionId),
|
|
205
|
+
configured_weight: Number(runtimeConfig.sessionWeights?.[sessionId] || 1),
|
|
206
|
+
status: row?.status || (socketRuntime.socket_open ? 'online' : 'offline'),
|
|
207
|
+
bot_jid: row?.botJid || socketRuntime.socket_bot_jid || null,
|
|
208
|
+
capacity_weight: Number(row?.capacityWeight || runtimeConfig.sessionWeights?.[sessionId] || 1),
|
|
209
|
+
current_score: Number(row?.currentScore || 0),
|
|
210
|
+
last_heartbeat_at: toIso(row?.lastHeartbeatAt),
|
|
211
|
+
last_connected_at: toIso(row?.lastConnectedAt),
|
|
212
|
+
last_disconnected_at: toIso(row?.lastDisconnectedAt),
|
|
213
|
+
updated_at: toIso(row?.updatedAt),
|
|
214
|
+
metadata: row?.metadata || null,
|
|
215
|
+
...socketRuntime,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
generated_at: new Date().toISOString(),
|
|
221
|
+
primary_session_id: runtimeConfig.primarySessionId,
|
|
222
|
+
configured_session_ids: runtimeConfig.sessionIds || [],
|
|
223
|
+
sessions,
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const listSystemAdminAssignments = async ({ groupJid = null, ownerSessionId = null, includeExpired = false, limit = 200 } = {}) => {
|
|
228
|
+
const safeGroupJid = normalizeOptional(groupJid, 255);
|
|
229
|
+
const safeOwnerSessionId = normalizeOptional(ownerSessionId, 64);
|
|
230
|
+
const safeIncludeExpired = normalizeBoolean(includeExpired, false);
|
|
231
|
+
const safeLimit = clampLimit(limit, 200, 1, 5_000);
|
|
232
|
+
|
|
233
|
+
const assignments = await groupOwnershipService.listAssignments({
|
|
234
|
+
groupJid: safeGroupJid,
|
|
235
|
+
ownerSessionId: safeOwnerSessionId,
|
|
236
|
+
includeExpired: safeIncludeExpired,
|
|
237
|
+
limit: safeLimit,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
generated_at: new Date().toISOString(),
|
|
242
|
+
filters: {
|
|
243
|
+
group_jid: safeGroupJid,
|
|
244
|
+
owner_session_id: safeOwnerSessionId,
|
|
245
|
+
include_expired: safeIncludeExpired,
|
|
246
|
+
limit: safeLimit,
|
|
247
|
+
},
|
|
248
|
+
assignments: (assignments || []).map((assignment) => ({
|
|
249
|
+
group_jid: assignment?.groupJid || null,
|
|
250
|
+
owner_session_id: assignment?.ownerSessionId || null,
|
|
251
|
+
lease_expires_at: toIso(assignment?.leaseExpiresAt),
|
|
252
|
+
cooldown_until: toIso(assignment?.cooldownUntil),
|
|
253
|
+
assignment_version: Number(assignment?.assignmentVersion || 1),
|
|
254
|
+
pinned: assignment?.pinned === true,
|
|
255
|
+
active: assignment?.active !== false,
|
|
256
|
+
last_reason: assignment?.lastReason || null,
|
|
257
|
+
created_at: toIso(assignment?.createdAt),
|
|
258
|
+
updated_at: toIso(assignment?.updatedAt),
|
|
259
|
+
})),
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export const setSystemAdminGroupPin = async ({ groupJid, pinned, sessionId = null, reason = null, changedBy = 'admin_api', metadata = null } = {}) => {
|
|
264
|
+
const outcome = await groupOwnershipService.setPinned({
|
|
265
|
+
groupJid,
|
|
266
|
+
pinned,
|
|
267
|
+
sessionId,
|
|
268
|
+
reason: reason || (pinned ? 'admin_pin_group' : 'admin_unpin_group'),
|
|
269
|
+
changedBy,
|
|
270
|
+
metadata,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
updated: Boolean(outcome?.updated),
|
|
275
|
+
reason: outcome?.reason || null,
|
|
276
|
+
assignment_version: Number(outcome?.assignmentVersion || 0) || null,
|
|
277
|
+
previous_owner_session_id: outcome?.previousOwnerSessionId || null,
|
|
278
|
+
owner: outcome?.owner
|
|
279
|
+
? {
|
|
280
|
+
group_jid: outcome.owner.groupJid,
|
|
281
|
+
owner_session_id: outcome.owner.ownerSessionId,
|
|
282
|
+
lease_expires_at: toIso(outcome.owner.leaseExpiresAt),
|
|
283
|
+
cooldown_until: toIso(outcome.owner.cooldownUntil),
|
|
284
|
+
assignment_version: Number(outcome.owner.assignmentVersion || 1),
|
|
285
|
+
pinned: outcome.owner.pinned === true,
|
|
286
|
+
last_reason: outcome.owner.lastReason || null,
|
|
287
|
+
}
|
|
288
|
+
: null,
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export const forceSystemAdminGroupFailover = async ({ groupJid, targetSessionId, reason = 'admin_force_failover', changedBy = 'admin_api', metadata = null } = {}) => {
|
|
293
|
+
const outcome = await groupOwnershipService.forceAssign({
|
|
294
|
+
groupJid,
|
|
295
|
+
sessionId: targetSessionId,
|
|
296
|
+
reason,
|
|
297
|
+
changedBy,
|
|
298
|
+
metadata,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
reassigned: Boolean(outcome?.reassigned),
|
|
303
|
+
reason: outcome?.reason || null,
|
|
304
|
+
assignment_version: Number(outcome?.assignmentVersion || 0) || null,
|
|
305
|
+
previous_owner_session_id: outcome?.previousOwnerSessionId || null,
|
|
306
|
+
owner: outcome?.owner
|
|
307
|
+
? {
|
|
308
|
+
group_jid: outcome.owner.groupJid,
|
|
309
|
+
owner_session_id: outcome.owner.ownerSessionId,
|
|
310
|
+
lease_expires_at: toIso(outcome.owner.leaseExpiresAt),
|
|
311
|
+
assignment_version: Number(outcome.owner.assignmentVersion || 1),
|
|
312
|
+
pinned: outcome.owner.pinned === true,
|
|
313
|
+
last_reason: outcome.owner.lastReason || null,
|
|
314
|
+
}
|
|
315
|
+
: null,
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export const triggerSystemAdminManualRebalance = async () => {
|
|
320
|
+
const cycle = await runGroupAssignmentBalancerCycle();
|
|
321
|
+
return {
|
|
322
|
+
generated_at: new Date().toISOString(),
|
|
323
|
+
cycle,
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
export const listSystemAdminAssignmentHistory = async ({ groupJid = null, limit = 100 } = {}) => {
|
|
328
|
+
const safeLimit = clampLimit(limit, 100, 1, 5_000);
|
|
329
|
+
const safeGroupJid = normalizeOptional(groupJid, 255);
|
|
330
|
+
const params = [];
|
|
331
|
+
const where = [];
|
|
332
|
+
if (safeGroupJid) {
|
|
333
|
+
where.push('group_jid = ?');
|
|
334
|
+
params.push(safeGroupJid);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const rows = await executeQuery(
|
|
338
|
+
`SELECT id, group_jid, previous_session_id, new_session_id, change_reason, changed_by, assignment_version, metadata, created_at
|
|
339
|
+
FROM ${TABLES.GROUP_ASSIGNMENT_HISTORY}
|
|
340
|
+
${where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''}
|
|
341
|
+
ORDER BY id DESC
|
|
342
|
+
LIMIT ${safeLimit}`,
|
|
343
|
+
params,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
generated_at: new Date().toISOString(),
|
|
348
|
+
history: (Array.isArray(rows) ? rows : []).map((row) => ({
|
|
349
|
+
id: Number(row?.id || 0),
|
|
350
|
+
group_jid: row?.group_jid || null,
|
|
351
|
+
previous_session_id: row?.previous_session_id || null,
|
|
352
|
+
new_session_id: row?.new_session_id || null,
|
|
353
|
+
change_reason: row?.change_reason || null,
|
|
354
|
+
changed_by: row?.changed_by || null,
|
|
355
|
+
assignment_version: Number(row?.assignment_version || 0) || null,
|
|
356
|
+
metadata: row?.metadata || null,
|
|
357
|
+
created_at: toIso(row?.created_at),
|
|
358
|
+
})),
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
135
362
|
export { scheduleGlobalRankingPreload };
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import logger from '#logger';
|
|
5
5
|
import { DEFAULT_LEGACY_STICKER_API_BASE_PATH, DEFAULT_USER_API_BASE_PATH, isUserApiPath, normalizeBasePath, resolveLegacyUserApiPath } from '../routes/user/userApiPaths.js';
|
|
6
|
+
import { buildWhatsappUrl, resolvePublicWhatsappNumber } from '../utils/publicContact.js';
|
|
6
7
|
|
|
7
8
|
const LEGACY_STICKER_API_BASE_PATH = normalizeBasePath(process.env.STICKER_API_BASE_PATH, DEFAULT_LEGACY_STICKER_API_BASE_PATH);
|
|
8
9
|
const USER_API_BASE_PATH = normalizeBasePath(process.env.USER_API_BASE_PATH || process.env.AUTH_API_BASE_PATH, DEFAULT_USER_API_BASE_PATH);
|
|
@@ -11,6 +12,8 @@ const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PAT
|
|
|
11
12
|
const USER_PASSWORD_RESET_WEB_PATH = normalizeBasePath(process.env.USER_PASSWORD_RESET_WEB_PATH, '/user/password-reset');
|
|
12
13
|
const USER_DASHBOARD_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'user.html');
|
|
13
14
|
const USER_PASSWORD_RESET_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'user-password-reset.html');
|
|
15
|
+
const PUBLIC_WHATSAPP_NUMBER = resolvePublicWhatsappNumber();
|
|
16
|
+
const PUBLIC_WHATSAPP_URL = buildWhatsappUrl(PUBLIC_WHATSAPP_NUMBER);
|
|
14
17
|
|
|
15
18
|
const hasPathPrefix = (pathname, prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
16
19
|
const escapeHtmlAttribute = (value) =>
|
|
@@ -52,7 +55,10 @@ const renderUserDashboardHtml = async ({ passwordReset = false } = {}) => {
|
|
|
52
55
|
const dataAttributes = {
|
|
53
56
|
'data-api-base-path': USER_API_BASE_PATH,
|
|
54
57
|
'data-login-path': STICKER_LOGIN_WEB_PATH,
|
|
58
|
+
'data-panel-path': USER_PROFILE_WEB_PATH,
|
|
55
59
|
'data-password-reset-web-path': USER_PASSWORD_RESET_WEB_PATH,
|
|
60
|
+
'data-support-whatsapp-number': PUBLIC_WHATSAPP_NUMBER,
|
|
61
|
+
'data-support-whatsapp-url': PUBLIC_WHATSAPP_URL,
|
|
56
62
|
};
|
|
57
63
|
|
|
58
64
|
let html = template;
|
|
@@ -38,6 +38,19 @@ let inFlight = false;
|
|
|
38
38
|
let timerHandle = null;
|
|
39
39
|
let nextDelayMs = EMAIL_AUTOMATION_POLL_INTERVAL_MS;
|
|
40
40
|
|
|
41
|
+
const maskEmailForLogs = (value) => {
|
|
42
|
+
const normalized = String(value || '')
|
|
43
|
+
.trim()
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.slice(0, 255);
|
|
46
|
+
const [localPartRaw, domainRaw] = normalized.split('@');
|
|
47
|
+
const localPart = String(localPartRaw || '').trim();
|
|
48
|
+
const domain = String(domainRaw || '').trim();
|
|
49
|
+
if (!localPart || !domain) return 'invalid-email';
|
|
50
|
+
const localMasked = localPart.length <= 2 ? `${localPart.charAt(0) || '*'}*` : `${localPart.slice(0, 2)}***`;
|
|
51
|
+
return `${localMasked}@${domain}`;
|
|
52
|
+
};
|
|
53
|
+
|
|
41
54
|
const applyDelayJitter = (delayMs) => {
|
|
42
55
|
const baseDelay = Math.max(250, Math.floor(Number(delayMs) || 0));
|
|
43
56
|
if (EMAIL_AUTOMATION_IDLE_JITTER_PERCENT <= 0) return baseDelay;
|
|
@@ -119,6 +132,16 @@ export const runEmailAutomationTick = async ({ maxPerTick = EMAIL_AUTOMATION_MAX
|
|
|
119
132
|
});
|
|
120
133
|
|
|
121
134
|
stats.sent += 1;
|
|
135
|
+
logger.debug('E-mail da fila entregue com sucesso.', {
|
|
136
|
+
action: 'email_automation_delivery_succeeded',
|
|
137
|
+
task_id: task.id,
|
|
138
|
+
template_key: task.template_key || null,
|
|
139
|
+
recipient_email_masked: maskEmailForLogs(task.recipient_email),
|
|
140
|
+
attempts: task.attempts,
|
|
141
|
+
max_attempts: task.max_attempts,
|
|
142
|
+
provider_message_id: delivery?.messageId || null,
|
|
143
|
+
provider_response: delivery?.response || null,
|
|
144
|
+
});
|
|
122
145
|
} catch (error) {
|
|
123
146
|
stats.failed += 1;
|
|
124
147
|
await failEmailOutboxTask(task.id, {
|
|
@@ -129,8 +152,12 @@ export const runEmailAutomationTick = async ({ maxPerTick = EMAIL_AUTOMATION_MAX
|
|
|
129
152
|
logger.warn('Falha ao entregar e-mail da fila.', {
|
|
130
153
|
action: 'email_automation_delivery_failed',
|
|
131
154
|
task_id: task.id,
|
|
132
|
-
|
|
155
|
+
template_key: task.template_key || null,
|
|
156
|
+
recipient_email_masked: maskEmailForLogs(task.recipient_email),
|
|
133
157
|
attempts: task.attempts,
|
|
158
|
+
max_attempts: task.max_attempts,
|
|
159
|
+
retry_delay_seconds: safeRetryDelay,
|
|
160
|
+
metadata_keys: Object.keys(task?.metadata || {}).slice(0, 20),
|
|
134
161
|
error: error?.message,
|
|
135
162
|
});
|
|
136
163
|
}
|
|
@@ -138,6 +165,14 @@ export const runEmailAutomationTick = async ({ maxPerTick = EMAIL_AUTOMATION_MAX
|
|
|
138
165
|
|
|
139
166
|
if (stats.claimed > 0) {
|
|
140
167
|
await refreshQueueDepthMetrics().catch(() => null);
|
|
168
|
+
logger.info('Processamento da fila de e-mail concluído.', {
|
|
169
|
+
action: 'email_automation_tick_processed',
|
|
170
|
+
claimed: stats.claimed,
|
|
171
|
+
sent: stats.sent,
|
|
172
|
+
failed: stats.failed,
|
|
173
|
+
max_per_tick: safeMaxPerTick,
|
|
174
|
+
retry_delay_seconds: safeRetryDelay,
|
|
175
|
+
});
|
|
141
176
|
}
|
|
142
177
|
|
|
143
178
|
return stats;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logger from '#logger';
|
|
1
2
|
import { enqueueEmailOutbox, getEmailOutboxStatusSnapshot } from './emailOutboxRepository.js';
|
|
2
3
|
import { renderEmailTemplate } from './emailTemplateService.js';
|
|
3
4
|
import { getEmailTransportMetadata } from './emailTransportService.js';
|
|
@@ -8,6 +9,22 @@ const normalizeEmail = (value) =>
|
|
|
8
9
|
.toLowerCase()
|
|
9
10
|
.slice(0, 255);
|
|
10
11
|
|
|
12
|
+
const maskEmailForLogs = (value) => {
|
|
13
|
+
const normalized = normalizeEmail(value);
|
|
14
|
+
const [localPartRaw, domainRaw] = normalized.split('@');
|
|
15
|
+
const localPart = String(localPartRaw || '').trim();
|
|
16
|
+
const domain = String(domainRaw || '').trim();
|
|
17
|
+
if (!localPart || !domain) return 'invalid-email';
|
|
18
|
+
const localMasked = localPart.length <= 2 ? `${localPart.charAt(0) || '*'}*` : `${localPart.slice(0, 2)}***`;
|
|
19
|
+
return `${localMasked}@${domain}`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const resolveEmailDomain = (value) => {
|
|
23
|
+
const normalized = normalizeEmail(value);
|
|
24
|
+
if (!normalized.includes('@')) return null;
|
|
25
|
+
return normalized.split('@')[1] || null;
|
|
26
|
+
};
|
|
27
|
+
|
|
11
28
|
const normalizeOptionalText = (value, maxLength = 500_000) => {
|
|
12
29
|
const normalized =
|
|
13
30
|
String(value || '')
|
|
@@ -31,6 +48,12 @@ const resolveEmailBodyFromPayload = ({ templateKey = '', templateData = {}, subj
|
|
|
31
48
|
|
|
32
49
|
const renderedTemplate = normalizedTemplateKey ? renderEmailTemplate(normalizedTemplateKey, normalizedTemplateData) : null;
|
|
33
50
|
|
|
51
|
+
if (normalizedTemplateKey && !renderedTemplate && !normalizeOptionalText(subject, 180) && !normalizeOptionalText(text, 120_000) && !normalizeOptionalText(html, 500_000)) {
|
|
52
|
+
const error = new Error(`Template de e-mail inválido ou indisponível: "${normalizedTemplateKey}".`);
|
|
53
|
+
error.statusCode = 400;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
const normalizedSubject = normalizeOptionalText(subject, 180) || renderedTemplate?.subject || '';
|
|
35
58
|
const normalizedText = normalizeOptionalText(text, 120_000) || renderedTemplate?.text || null;
|
|
36
59
|
const normalizedHtml = normalizeOptionalText(html, 500_000) || renderedTemplate?.html || null;
|
|
@@ -72,6 +95,8 @@ export const queueAutomatedEmail = async ({ to, name = '', templateKey = '', tem
|
|
|
72
95
|
html,
|
|
73
96
|
});
|
|
74
97
|
|
|
98
|
+
const safeMetadata = normalizePayloadObject(metadata);
|
|
99
|
+
|
|
75
100
|
const taskId = await enqueueEmailOutbox({
|
|
76
101
|
recipientEmail: normalizedEmail,
|
|
77
102
|
recipientName: normalizeOptionalText(name, 120),
|
|
@@ -80,7 +105,7 @@ export const queueAutomatedEmail = async ({ to, name = '', templateKey = '', tem
|
|
|
80
105
|
htmlBody: body.html_body,
|
|
81
106
|
templateKey: body.template_key,
|
|
82
107
|
templatePayload: body.template_payload,
|
|
83
|
-
metadata:
|
|
108
|
+
metadata: safeMetadata,
|
|
84
109
|
priority,
|
|
85
110
|
scheduledAt,
|
|
86
111
|
maxAttempts,
|
|
@@ -93,6 +118,22 @@ export const queueAutomatedEmail = async ({ to, name = '', templateKey = '', tem
|
|
|
93
118
|
throw error;
|
|
94
119
|
}
|
|
95
120
|
|
|
121
|
+
logger.info('E-mail enfileirado para processamento.', {
|
|
122
|
+
action: 'email_outbox_enqueued',
|
|
123
|
+
task_id: taskId,
|
|
124
|
+
template_key: body.template_key || null,
|
|
125
|
+
recipient_email_masked: maskEmailForLogs(normalizedEmail),
|
|
126
|
+
recipient_domain: resolveEmailDomain(normalizedEmail),
|
|
127
|
+
subject_length: body.subject.length,
|
|
128
|
+
has_text_body: Boolean(body.text_body),
|
|
129
|
+
has_html_body: Boolean(body.html_body),
|
|
130
|
+
metadata_keys: Object.keys(safeMetadata).slice(0, 20),
|
|
131
|
+
priority: Number.isFinite(Number(priority)) ? Number(priority) : null,
|
|
132
|
+
scheduled_at: scheduledAt ? String(scheduledAt) : null,
|
|
133
|
+
max_attempts: Number.isFinite(Number(maxAttempts)) ? Number(maxAttempts) : null,
|
|
134
|
+
idempotency_key_present: Boolean(String(idempotencyKey || '').trim()),
|
|
135
|
+
});
|
|
136
|
+
|
|
96
137
|
return {
|
|
97
138
|
task_id: taskId,
|
|
98
139
|
recipient_email: normalizedEmail,
|