@kaikybrofc/omnizap-system 2.3.1 → 2.3.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/README.md +82 -483
- package/app/controllers/messageController.js +473 -255
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/stickerModule/stickerCommand.js +7 -2
- package/app/modules/stickerModule/stickerTextCommand.js +7 -2
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +1 -3
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +224 -53
- package/app/observability/metrics.js +6 -3
- package/app/services/googleWebLinkService.js +77 -0
- package/app/services/lidMapService.js +83 -4
- package/database/index.js +2 -0
- package/database/migrations/20260301_0028_message_analysis_event.sql +32 -0
- package/database/migrations/20260301_0029_admin_action_audit.sql +16 -0
- package/package.json +1 -1
- package/public/index.html +12 -8
- package/public/js/apps/createPackApp.js +4 -4
- package/public/js/apps/homeApp.js +78 -34
- package/public/js/apps/loginApp.js +245 -35
- package/public/js/apps/stickersAdminApp.js +4 -10
- package/public/js/apps/stickersApp.js +1 -1
- package/public/js/apps/userApp.js +956 -55
- package/public/js/apps/userProfileApp.js +244 -0
- package/public/login/index.html +437 -101
- package/public/termos-de-uso/index.html +1 -1
- package/public/user/index.html +2 -181
- package/public/user/systemadm/index.html +774 -0
- package/server/controllers/stickerCatalog/nonCatalogHandlers.js +183 -0
- package/server/controllers/stickerCatalogController.js +1289 -368
- package/server/controllers/systemAdminController.js +141 -0
- package/server/controllers/userController.js +87 -0
- package/server/http/httpServer.js +72 -32
- package/server/middleware/cachePolicy.js +24 -0
- package/server/middleware/cachePolicyHelpers.js +1 -0
- package/server/middleware/rateLimit.js +89 -0
- package/server/middleware/requestLogger.js +16 -0
- package/server/middleware/requireAdminAuth.js +42 -0
- package/server/middleware/securityHeaders.js +6 -0
- package/server/routes/admin/systemAdminRouter.js +56 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +197 -0
- package/server/routes/metrics/metricsRouter.js +13 -0
- package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +44 -0
- package/server/routes/stickerCatalog/stickerApiRouter.js +84 -0
- package/server/routes/stickerCatalog/stickerDataRouter.js +140 -0
- package/server/routes/stickerCatalog/stickerSiteRouter.js +43 -0
- package/server/routes/user/userRouter.js +56 -0
- package/server/utils/safePath.js +26 -0
- package/server/routes/metricsRoute.js +0 -7
- package/server/routes/stickerCatalogRoute.js +0 -20
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
1
4
|
import logger from '../utils/logger/loggerModule.js';
|
|
2
5
|
import { executeQuery, TABLES } from '../../database/index.js';
|
|
3
6
|
import { getJidServer, normalizeJid, isGroupJid } from '../config/baileysConfig.js';
|
|
@@ -10,12 +13,16 @@ const STORE_COOLDOWN_MS = 10 * 60 * 1000;
|
|
|
10
13
|
const BATCH_LIMIT = 800;
|
|
11
14
|
const BACKFILL_DEFAULT_BATCH = 50000;
|
|
12
15
|
const BACKFILL_SOURCE = 'backfill';
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
const BAILEYS_AUTH_DIR = path.resolve(__dirname, '../connection/auth');
|
|
13
19
|
|
|
14
20
|
const LID_SERVERS = new Set(['lid', 'hosted.lid']);
|
|
15
21
|
const PN_SERVERS = new Set(['s.whatsapp.net', 'c.us', 'hosted']);
|
|
16
22
|
|
|
17
23
|
const lidCache = new Map();
|
|
18
24
|
const lidWriteBuffer = new Map();
|
|
25
|
+
const authReverseLidCache = new Map();
|
|
19
26
|
|
|
20
27
|
let backfillPromise = null;
|
|
21
28
|
|
|
@@ -55,6 +62,54 @@ const normalizeWhatsAppJid = (jid) => {
|
|
|
55
62
|
return normalized || null;
|
|
56
63
|
};
|
|
57
64
|
|
|
65
|
+
const toDigits = (value) => String(value || '').replace(/\D+/g, '');
|
|
66
|
+
|
|
67
|
+
const parseReverseMappingPhoneDigits = (content) => {
|
|
68
|
+
const raw = String(content || '').trim();
|
|
69
|
+
if (!raw) return '';
|
|
70
|
+
|
|
71
|
+
let parsed = raw;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(raw);
|
|
74
|
+
} catch {
|
|
75
|
+
parsed = raw;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const digits = toDigits(parsed);
|
|
79
|
+
return digits.length >= 10 && digits.length <= 15 ? digits : '';
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const resolveAuthStoreJidByLid = async (lid) => {
|
|
83
|
+
const normalizedLid = normalizeLid(lid);
|
|
84
|
+
if (!normalizedLid) return null;
|
|
85
|
+
|
|
86
|
+
const [rawUser] = normalizedLid.split('@');
|
|
87
|
+
const rootUser = rawUser ? rawUser.split(':')[0] : '';
|
|
88
|
+
if (!rootUser || !/^\d+$/.test(rootUser)) return null;
|
|
89
|
+
|
|
90
|
+
if (authReverseLidCache.has(rootUser)) {
|
|
91
|
+
return authReverseLidCache.get(rootUser);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const reverseFilePath = path.join(BAILEYS_AUTH_DIR, `lid-mapping-${rootUser}_reverse.json`);
|
|
95
|
+
try {
|
|
96
|
+
const content = await readFile(reverseFilePath, 'utf8');
|
|
97
|
+
const phoneDigits = parseReverseMappingPhoneDigits(content);
|
|
98
|
+
const resolvedJid = phoneDigits ? normalizeWhatsAppJid(`${phoneDigits}@s.whatsapp.net`) : null;
|
|
99
|
+
authReverseLidCache.set(rootUser, resolvedJid);
|
|
100
|
+
return resolvedJid;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error?.code !== 'ENOENT') {
|
|
103
|
+
logger.warn('Falha ao resolver LID via auth store local.', {
|
|
104
|
+
lid: normalizedLid,
|
|
105
|
+
error: error?.message,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
authReverseLidCache.set(rootUser, null);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
58
113
|
/**
|
|
59
114
|
* Mascara um JID para logs.
|
|
60
115
|
* @param {string|null|undefined} jid
|
|
@@ -191,8 +246,21 @@ export const primeLidCache = async (lids = []) => {
|
|
|
191
246
|
const base = baseByLid.get(lid);
|
|
192
247
|
const direct = rowMap.has(lid) ? rowMap.get(lid) : undefined;
|
|
193
248
|
const baseValue = base && base !== lid && rowMap.has(base) ? rowMap.get(base) : undefined;
|
|
194
|
-
|
|
195
|
-
|
|
249
|
+
let resolved = direct ?? baseValue ?? null;
|
|
250
|
+
|
|
251
|
+
if (!resolved) {
|
|
252
|
+
const authStoreResolved = await resolveAuthStoreJidByLid(lid);
|
|
253
|
+
if (authStoreResolved) {
|
|
254
|
+
resolved = authStoreResolved;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const directHasJid = typeof direct === 'string' && direct.length > 0;
|
|
259
|
+
const shouldSeed = Boolean(resolved && (!directHasJid || direct !== resolved));
|
|
260
|
+
setCacheEntry(lid, resolved, resolved ? CACHE_TTL_MS : NEGATIVE_TTL_MS, shouldSeed ? 0 : undefined);
|
|
261
|
+
if (shouldSeed) {
|
|
262
|
+
queueLidUpdate(lid, resolved, 'prime');
|
|
263
|
+
}
|
|
196
264
|
results.set(lid, resolved);
|
|
197
265
|
}
|
|
198
266
|
|
|
@@ -379,6 +447,15 @@ const fetchJidByLid = async (lid) => {
|
|
|
379
447
|
const direct = rowMap.has(lid) ? rowMap.get(lid) : undefined;
|
|
380
448
|
const baseValue = base && base !== lid && rowMap.has(base) ? rowMap.get(base) : undefined;
|
|
381
449
|
let resolved = direct ?? baseValue ?? null;
|
|
450
|
+
let resolveSource = 'db';
|
|
451
|
+
|
|
452
|
+
if (!resolved) {
|
|
453
|
+
const authStoreResolved = await resolveAuthStoreJidByLid(lid);
|
|
454
|
+
if (authStoreResolved) {
|
|
455
|
+
resolved = authStoreResolved;
|
|
456
|
+
resolveSource = 'auth-store';
|
|
457
|
+
}
|
|
458
|
+
}
|
|
382
459
|
|
|
383
460
|
if (!resolved) {
|
|
384
461
|
const normalized = base || lid;
|
|
@@ -404,16 +481,18 @@ const fetchJidByLid = async (lid) => {
|
|
|
404
481
|
const derivedJid = derivedRows?.[0]?.jid;
|
|
405
482
|
if (derivedJid && isWhatsAppJid(derivedJid)) {
|
|
406
483
|
resolved = normalizeJid(derivedJid);
|
|
484
|
+
resolveSource = 'derived';
|
|
407
485
|
}
|
|
408
486
|
}
|
|
409
487
|
}
|
|
410
488
|
|
|
411
|
-
const
|
|
489
|
+
const directHasJid = typeof direct === 'string' && direct.length > 0;
|
|
490
|
+
const shouldSeedDerived = Boolean(resolved && (!directHasJid || direct !== resolved));
|
|
412
491
|
|
|
413
492
|
setCacheEntry(lid, resolved, resolved ? CACHE_TTL_MS : NEGATIVE_TTL_MS, shouldSeedDerived ? 0 : undefined);
|
|
414
493
|
|
|
415
494
|
if (shouldSeedDerived) {
|
|
416
|
-
queueLidUpdate(lid, resolved, 'derived');
|
|
495
|
+
queueLidUpdate(lid, resolved, resolveSource === 'auth-store' ? 'auth-store' : 'derived');
|
|
417
496
|
}
|
|
418
497
|
|
|
419
498
|
return resolved;
|
package/database/index.js
CHANGED
|
@@ -91,6 +91,7 @@ logger.info(`Configuração de banco de dados carregada para o ambiente: ${envir
|
|
|
91
91
|
*/
|
|
92
92
|
export const TABLES = {
|
|
93
93
|
MESSAGES: 'messages',
|
|
94
|
+
MESSAGE_ANALYSIS_EVENT: 'message_analysis_event',
|
|
94
95
|
CHATS: 'chats',
|
|
95
96
|
GROUPS_METADATA: 'groups_metadata',
|
|
96
97
|
GROUP_CONFIGS: 'group_configs',
|
|
@@ -116,6 +117,7 @@ export const TABLES = {
|
|
|
116
117
|
STICKER_WEB_GOOGLE_SESSION: 'sticker_web_google_session',
|
|
117
118
|
STICKER_WEB_ADMIN_BAN: 'sticker_web_admin_ban',
|
|
118
119
|
STICKER_WEB_ADMIN_MODERATOR: 'sticker_web_admin_moderator',
|
|
120
|
+
ADMIN_ACTION_AUDIT: 'admin_action_audit',
|
|
119
121
|
RPG_PLAYER: 'rpg_player',
|
|
120
122
|
RPG_PLAYER_POKEMON: 'rpg_player_pokemon',
|
|
121
123
|
RPG_BATTLE_STATE: 'rpg_battle_state',
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS message_analysis_event (
|
|
2
|
+
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
|
3
|
+
message_id VARCHAR(255) NULL,
|
|
4
|
+
chat_id VARCHAR(255) NULL,
|
|
5
|
+
sender_id VARCHAR(255) NULL,
|
|
6
|
+
sender_name VARCHAR(120) NULL,
|
|
7
|
+
upsert_type VARCHAR(32) NULL,
|
|
8
|
+
source VARCHAR(32) NOT NULL DEFAULT 'whatsapp',
|
|
9
|
+
is_group TINYINT(1) NOT NULL DEFAULT 0,
|
|
10
|
+
is_from_bot TINYINT(1) NOT NULL DEFAULT 0,
|
|
11
|
+
is_command TINYINT(1) NOT NULL DEFAULT 0,
|
|
12
|
+
command_name VARCHAR(64) NULL,
|
|
13
|
+
command_args_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
|
14
|
+
command_known TINYINT(1) NULL,
|
|
15
|
+
command_prefix VARCHAR(8) NULL,
|
|
16
|
+
message_kind VARCHAR(48) NOT NULL DEFAULT 'other',
|
|
17
|
+
has_media TINYINT(1) NOT NULL DEFAULT 0,
|
|
18
|
+
media_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
|
19
|
+
text_length INT UNSIGNED NOT NULL DEFAULT 0,
|
|
20
|
+
processing_result VARCHAR(64) NOT NULL DEFAULT 'processed',
|
|
21
|
+
error_code VARCHAR(96) NULL,
|
|
22
|
+
metadata JSON NULL,
|
|
23
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
INDEX idx_message_analysis_created (created_at),
|
|
25
|
+
INDEX idx_message_analysis_chat_created (chat_id, created_at),
|
|
26
|
+
INDEX idx_message_analysis_sender_created (sender_id, created_at),
|
|
27
|
+
INDEX idx_message_analysis_command_created (command_name, created_at),
|
|
28
|
+
INDEX idx_message_analysis_kind_created (message_kind, created_at),
|
|
29
|
+
INDEX idx_message_analysis_result_created (processing_result, created_at),
|
|
30
|
+
INDEX idx_message_analysis_is_command_created (is_command, created_at)
|
|
31
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
32
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS admin_action_audit (
|
|
2
|
+
id CHAR(36) PRIMARY KEY,
|
|
3
|
+
admin_role VARCHAR(32) NOT NULL DEFAULT 'owner',
|
|
4
|
+
admin_google_sub VARCHAR(255) NULL,
|
|
5
|
+
admin_email VARCHAR(255) NULL,
|
|
6
|
+
admin_owner_jid VARCHAR(255) NULL,
|
|
7
|
+
action VARCHAR(96) NOT NULL,
|
|
8
|
+
target_type VARCHAR(64) NULL,
|
|
9
|
+
target_id VARCHAR(255) NULL,
|
|
10
|
+
status VARCHAR(32) NOT NULL DEFAULT 'success',
|
|
11
|
+
details JSON NULL,
|
|
12
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
13
|
+
INDEX idx_admin_action_audit_created (created_at),
|
|
14
|
+
INDEX idx_admin_action_audit_action_created (action, created_at),
|
|
15
|
+
INDEX idx_admin_action_audit_admin_created (admin_google_sub, admin_email, created_at)
|
|
16
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -395,7 +395,7 @@
|
|
|
395
395
|
.proof-grid {
|
|
396
396
|
margin-top: 16px;
|
|
397
397
|
display: grid;
|
|
398
|
-
grid-template-columns: repeat(
|
|
398
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
399
399
|
gap: 10px;
|
|
400
400
|
}
|
|
401
401
|
|
|
@@ -879,16 +879,20 @@
|
|
|
879
879
|
|
|
880
880
|
<div class="proof-grid" aria-label="Prova social">
|
|
881
881
|
<article class="proof">
|
|
882
|
-
<strong id="proof-
|
|
883
|
-
<span>
|
|
882
|
+
<strong id="proof-bots-online">--</strong>
|
|
883
|
+
<span>bots online agora</span>
|
|
884
884
|
</article>
|
|
885
885
|
<article class="proof">
|
|
886
|
-
<strong id="proof-
|
|
887
|
-
<span>
|
|
886
|
+
<strong id="proof-messages-today">--</strong>
|
|
887
|
+
<span>mensagens hoje</span>
|
|
888
888
|
</article>
|
|
889
889
|
<article class="proof">
|
|
890
|
-
<strong id="proof-
|
|
891
|
-
<span>
|
|
890
|
+
<strong id="proof-spam-blocked">--</strong>
|
|
891
|
+
<span>spam bloqueado hoje</span>
|
|
892
|
+
</article>
|
|
893
|
+
<article class="proof">
|
|
894
|
+
<strong id="proof-uptime">--</strong>
|
|
895
|
+
<span>uptime do bot</span>
|
|
892
896
|
</article>
|
|
893
897
|
</div>
|
|
894
898
|
</div>
|
|
@@ -1132,6 +1136,6 @@
|
|
|
1132
1136
|
><svg viewBox="0 0 24 24"><use href="#icon-whatsapp"></use></svg></span
|
|
1133
1137
|
></a>
|
|
1134
1138
|
|
|
1135
|
-
<script type="module" src="/js/apps/homeApp.js?v=
|
|
1139
|
+
<script type="module" src="/js/apps/homeApp.js?v=20260301-home-realtime-v11"></script>
|
|
1136
1140
|
</body>
|
|
1137
1141
|
</html>
|
|
@@ -141,7 +141,7 @@ const clearGoogleAuthCache = () => {
|
|
|
141
141
|
};
|
|
142
142
|
|
|
143
143
|
const fetchJson = async (url, options = {}) => {
|
|
144
|
-
const response = await fetch(url, { credentials: '
|
|
144
|
+
const response = await fetch(url, { credentials: 'include', ...options });
|
|
145
145
|
const payload = await response.json().catch(() => ({}));
|
|
146
146
|
if (!response.ok) {
|
|
147
147
|
throw new Error(payload?.error || 'Falha na requisição.');
|
|
@@ -730,7 +730,7 @@ function CreatePackApp() {
|
|
|
730
730
|
try {
|
|
731
731
|
const response = await fetch(`${apiBasePath}/${encodeURIComponent(activeSession.packKey)}/publish-state`, {
|
|
732
732
|
method: 'POST',
|
|
733
|
-
credentials: '
|
|
733
|
+
credentials: 'include',
|
|
734
734
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
735
735
|
body: JSON.stringify({ edit_token: activeSession.editToken }),
|
|
736
736
|
});
|
|
@@ -958,7 +958,7 @@ function CreatePackApp() {
|
|
|
958
958
|
|
|
959
959
|
const createResponse = await fetch(`${apiBasePath}/create`, {
|
|
960
960
|
method: 'POST',
|
|
961
|
-
credentials: '
|
|
961
|
+
credentials: 'include',
|
|
962
962
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
963
963
|
body: JSON.stringify({
|
|
964
964
|
name: finalName,
|
|
@@ -1151,7 +1151,7 @@ function CreatePackApp() {
|
|
|
1151
1151
|
setStatus('Publicando pack...');
|
|
1152
1152
|
const finalizeResponse = await fetch(`${apiBasePath}/${encodeURIComponent(session.packKey)}/finalize`, {
|
|
1153
1153
|
method: 'POST',
|
|
1154
|
-
credentials: '
|
|
1154
|
+
credentials: 'include',
|
|
1155
1155
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
1156
1156
|
body: JSON.stringify({ edit_token: session.editToken }),
|
|
1157
1157
|
});
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
const FALLBACK_THUMB_URL = '/assets/images/brand-logo-128.webp';
|
|
2
2
|
const HOME_BOOTSTRAP_ENDPOINT = '/api/sticker-packs/home-bootstrap';
|
|
3
3
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
4
|
+
const SOCIAL_PROOF_REFRESH_MS = 15_000;
|
|
4
5
|
let homeBootstrapPayloadPromise = null;
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const loadHomeBootstrapPayload = async () => {
|
|
8
|
+
const response = await fetch(HOME_BOOTSTRAP_ENDPOINT, { credentials: 'include' });
|
|
9
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
10
|
+
const payload = await response.json();
|
|
11
|
+
return payload?.data || {};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const fetchHomeBootstrapPayload = async ({ forceRefresh = false } = {}) => {
|
|
15
|
+
if (forceRefresh) {
|
|
16
|
+
const freshData = await loadHomeBootstrapPayload();
|
|
17
|
+
homeBootstrapPayloadPromise = Promise.resolve(freshData);
|
|
18
|
+
return freshData;
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
if (!homeBootstrapPayloadPromise) {
|
|
8
|
-
homeBootstrapPayloadPromise =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
})
|
|
13
|
-
.then((payload) => payload?.data || {})
|
|
14
|
-
.catch((error) => {
|
|
15
|
-
homeBootstrapPayloadPromise = null;
|
|
16
|
-
throw error;
|
|
17
|
-
});
|
|
22
|
+
homeBootstrapPayloadPromise = loadHomeBootstrapPayload().catch((error) => {
|
|
23
|
+
homeBootstrapPayloadPromise = null;
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
18
26
|
}
|
|
19
27
|
return homeBootstrapPayloadPromise;
|
|
20
28
|
};
|
|
@@ -454,17 +462,19 @@ const initAddBotCtas = () => {
|
|
|
454
462
|
};
|
|
455
463
|
|
|
456
464
|
const initSocialProof = () => {
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
const
|
|
465
|
+
const botsOnlineEl = document.getElementById('proof-bots-online');
|
|
466
|
+
const messagesTodayEl = document.getElementById('proof-messages-today');
|
|
467
|
+
const spamBlockedEl = document.getElementById('proof-spam-blocked');
|
|
468
|
+
const uptimeEl = document.getElementById('proof-uptime');
|
|
460
469
|
const statusEl = document.getElementById('proof-status');
|
|
461
470
|
|
|
462
|
-
if (!
|
|
471
|
+
if (!botsOnlineEl || !messagesTodayEl || !spamBlockedEl || !uptimeEl) return null;
|
|
463
472
|
|
|
464
473
|
const setFallback = () => {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
474
|
+
botsOnlineEl.textContent = 'n/d';
|
|
475
|
+
messagesTodayEl.textContent = 'n/d';
|
|
476
|
+
spamBlockedEl.textContent = 'n/d';
|
|
477
|
+
uptimeEl.textContent = 'n/d';
|
|
468
478
|
if (statusEl) statusEl.textContent = 'bot pronto';
|
|
469
479
|
};
|
|
470
480
|
|
|
@@ -479,27 +489,61 @@ const initSocialProof = () => {
|
|
|
479
489
|
return 'pronto';
|
|
480
490
|
};
|
|
481
491
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
492
|
+
const setNumericMetric = (element, value, { animate = true } = {}) => {
|
|
493
|
+
const hasValue = value !== null && value !== undefined && value !== '';
|
|
494
|
+
const numeric = hasValue ? Number(value) : Number.NaN;
|
|
495
|
+
if (!hasValue || !Number.isFinite(numeric)) {
|
|
496
|
+
element.textContent = 'n/d';
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (!animate) {
|
|
500
|
+
element.textContent = shortNum(numeric);
|
|
501
|
+
element.dataset.value = String(Math.max(0, numeric));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
animateCountUp(element, numeric);
|
|
505
|
+
};
|
|
488
506
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
507
|
+
const refreshMetrics = async ({ forceRefresh = false, animate = false } = {}) => {
|
|
508
|
+
const bootstrapData = await fetchHomeBootstrapPayload({ forceRefresh });
|
|
509
|
+
const summary = bootstrapData?.system_summary || {};
|
|
510
|
+
const realtime = bootstrapData?.home_realtime || {};
|
|
492
511
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
512
|
+
const botsOnline = Number(realtime?.bots_online);
|
|
513
|
+
const messagesToday = Number(realtime?.messages_today);
|
|
514
|
+
const spamBlockedToday = Number(realtime?.spam_blocked_today);
|
|
515
|
+
const uptime = String(realtime?.uptime || summary?.process?.uptime || '').trim() || 'n/d';
|
|
516
|
+
|
|
517
|
+
setNumericMetric(botsOnlineEl, botsOnline, { animate });
|
|
518
|
+
setNumericMetric(messagesTodayEl, messagesToday, { animate });
|
|
519
|
+
setNumericMetric(spamBlockedEl, spamBlockedToday, { animate });
|
|
520
|
+
uptimeEl.textContent = uptime;
|
|
521
|
+
|
|
522
|
+
if (statusEl) {
|
|
523
|
+
statusEl.textContent = `bot ${normalizeStatus(summary?.system_status || summary?.bot?.connection_status)}`;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
let intervalId = null;
|
|
528
|
+
const stopBootstrap = runAfterLoadIdle(
|
|
529
|
+
() => {
|
|
530
|
+
void refreshMetrics({ forceRefresh: false, animate: true }).catch(() => {
|
|
531
|
+
setFallback();
|
|
532
|
+
});
|
|
500
533
|
},
|
|
501
534
|
{ delayMs: 620, timeoutMs: 1500 },
|
|
502
535
|
);
|
|
536
|
+
|
|
537
|
+
intervalId = window.setInterval(() => {
|
|
538
|
+
void refreshMetrics({ forceRefresh: true, animate: false }).catch(() => {});
|
|
539
|
+
}, SOCIAL_PROOF_REFRESH_MS);
|
|
540
|
+
|
|
541
|
+
return () => {
|
|
542
|
+
stopBootstrap();
|
|
543
|
+
if (intervalId !== null) {
|
|
544
|
+
window.clearInterval(intervalId);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
503
547
|
};
|
|
504
548
|
|
|
505
549
|
const registerCleanup = (cleanups, cleanup) => {
|