@kaikybrofc/omnizap-system 2.1.8
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 +534 -0
- package/LICENSE +21 -0
- package/README.md +431 -0
- package/RELEASE-v2.1.2.md +83 -0
- package/app/config/adminIdentity.js +87 -0
- package/app/config/baileysConfig.js +693 -0
- package/app/config/groupUtils.js +388 -0
- package/app/connection/socketController.js +992 -0
- package/app/controllers/messageController.js +354 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
- package/app/modules/adminModule/groupEventHandlers.js +355 -0
- package/app/modules/aiModule/catCommand.js +1006 -0
- package/app/modules/broadcastModule/noticeCommand.js +416 -0
- package/app/modules/gameModule/diceCommand.js +67 -0
- package/app/modules/menuModule/common.js +311 -0
- package/app/modules/menuModule/menus.js +59 -0
- package/app/modules/playModule/playCommand.js +1615 -0
- package/app/modules/quoteModule/quoteCommand.js +851 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/globalRankingCommand.js +65 -0
- package/app/modules/statsModule/noMessageCommand.js +288 -0
- package/app/modules/statsModule/rankingCommand.js +60 -0
- package/app/modules/statsModule/rankingCommon.js +889 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/convertToWebp.js +390 -0
- package/app/modules/stickerModule/stickerCommand.js +454 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
- package/app/modules/stickerModule/stickerTextCommand.js +657 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
- package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
- package/app/modules/stickerPackModule/stickerPackService.js +788 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
- package/app/modules/systemMetricsModule/pingCommand.js +421 -0
- package/app/modules/tiktokModule/tiktokCommand.js +798 -0
- package/app/modules/userModule/userCommand.js +1217 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
- package/app/observability/metrics.js +734 -0
- package/app/services/captchaService.js +492 -0
- package/app/services/dbWriteQueue.js +572 -0
- package/app/services/groupMetadataService.js +279 -0
- package/app/services/lidMapService.js +663 -0
- package/app/services/messagePersistenceService.js +56 -0
- package/app/services/newsBroadcastService.js +351 -0
- package/app/services/pokeApiService.js +398 -0
- package/app/services/queueUtils.js +57 -0
- package/app/services/socketState.js +7 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +36 -0
- package/app/utils/antiLink/antiLinkModule.js +804 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/logger/loggerModule.js +262 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
- package/database/index.js +2052 -0
- package/database/init.js +516 -0
- package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
- package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
- package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
- package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
- package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
- package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
- package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
- package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
- package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
- package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
- package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
- package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
- package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
- package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
- package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
- package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
- package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
- package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
- package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
- package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
- package/docker-compose.yml +103 -0
- package/ecosystem.prod.config.cjs +35 -0
- package/eslint.config.js +61 -0
- package/index.js +437 -0
- package/ml/clip_classifier/Dockerfile +16 -0
- package/ml/clip_classifier/README.md +120 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-exporter.cnf +5 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +32 -0
- package/observability/promtail-config.yml +84 -0
- package/package.json +109 -0
- package/public/api-docs/index.html +144 -0
- package/public/css/github-project-panel.css +297 -0
- package/public/css/stickers-admin.css +1272 -0
- package/public/css/styles.css +671 -0
- package/public/index.html +1311 -0
- package/public/js/apps/apiDocsApp.js +310 -0
- package/public/js/apps/createPackApp.js +2069 -0
- package/public/js/apps/homeApp.js +396 -0
- package/public/js/apps/stickersAdminApp.js +1744 -0
- package/public/js/apps/stickersApp.js +4830 -0
- package/public/js/catalog.js +1019 -0
- package/public/js/github-panel/components/CommitList.js +34 -0
- package/public/js/github-panel/components/ErrorState.js +16 -0
- package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
- package/public/js/github-panel/components/ReleaseList.js +38 -0
- package/public/js/github-panel/components/SkeletonPanel.js +22 -0
- package/public/js/github-panel/components/StatCard.js +15 -0
- package/public/js/github-panel/index.js +15 -0
- package/public/js/github-panel/useGithubRepoData.js +154 -0
- package/public/js/github-panel/vendor/react.js +11 -0
- package/public/js/runtime/react-runtime.js +19 -0
- package/public/licenca/index.html +106 -0
- package/public/stickers/admin/index.html +23 -0
- package/public/stickers/create/index.html +47 -0
- package/public/stickers/index.html +48 -0
- package/public/termos-de-uso/index.html +125 -0
- package/scripts/cache-bust.mjs +107 -0
- package/scripts/deploy.sh +458 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/release.sh +129 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import logger from '../utils/logger/loggerModule.js';
|
|
2
|
+
import { executeQuery, TABLES } from '../../database/index.js';
|
|
3
|
+
import { getJidServer, normalizeJid, isGroupJid } from '../config/baileysConfig.js';
|
|
4
|
+
import { buildRowPlaceholders, createFlushRunner } from './queueUtils.js';
|
|
5
|
+
import { recordError, setQueueDepth } from '../observability/metrics.js';
|
|
6
|
+
|
|
7
|
+
const CACHE_TTL_MS = 20 * 60 * 1000;
|
|
8
|
+
const NEGATIVE_TTL_MS = 5 * 60 * 1000;
|
|
9
|
+
const STORE_COOLDOWN_MS = 10 * 60 * 1000;
|
|
10
|
+
const BATCH_LIMIT = 800;
|
|
11
|
+
const BACKFILL_DEFAULT_BATCH = 50000;
|
|
12
|
+
const BACKFILL_SOURCE = 'backfill';
|
|
13
|
+
|
|
14
|
+
const LID_SERVERS = new Set(['lid', 'hosted.lid']);
|
|
15
|
+
const PN_SERVERS = new Set(['s.whatsapp.net', 'c.us', 'hosted']);
|
|
16
|
+
|
|
17
|
+
const lidCache = new Map();
|
|
18
|
+
const lidWriteBuffer = new Map();
|
|
19
|
+
|
|
20
|
+
let backfillPromise = null;
|
|
21
|
+
|
|
22
|
+
const updateLidQueueMetric = () => {
|
|
23
|
+
setQueueDepth('lid_map', lidWriteBuffer.size);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Retorna timestamp atual em ms.
|
|
28
|
+
* @returns {number}
|
|
29
|
+
*/
|
|
30
|
+
const now = () => Date.now();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Verifica se o JID e do tipo LID (lid/hosted.lid).
|
|
34
|
+
* @param {string|null|undefined} jid
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
const isLidJid = (jid) => LID_SERVERS.has(getJidServer(jid));
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Verifica se o JID e do WhatsApp (s.whatsapp.net/c.us/hosted).
|
|
41
|
+
* @param {string|null|undefined} jid
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
const isWhatsAppJid = (jid) => PN_SERVERS.has(getJidServer(jid));
|
|
45
|
+
|
|
46
|
+
const normalizeLid = (lid) => {
|
|
47
|
+
if (!lid || !isLidJid(lid)) return null;
|
|
48
|
+
const normalized = normalizeJid(lid);
|
|
49
|
+
return normalized || null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const normalizeWhatsAppJid = (jid) => {
|
|
53
|
+
if (!jid || !isWhatsAppJid(jid)) return null;
|
|
54
|
+
const normalized = normalizeJid(jid);
|
|
55
|
+
return normalized || null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mascara um JID para logs.
|
|
60
|
+
* @param {string|null|undefined} jid
|
|
61
|
+
* @returns {string|null}
|
|
62
|
+
*/
|
|
63
|
+
const maskJid = (jid) => {
|
|
64
|
+
if (!jid || typeof jid !== 'string') return null;
|
|
65
|
+
const [user, server] = jid.split('@');
|
|
66
|
+
if (!user || !server) return jid;
|
|
67
|
+
const head = user.slice(0, 3);
|
|
68
|
+
const tail = user.slice(-2);
|
|
69
|
+
return `${head}***${tail}@${server}`;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Busca entrada do cache (com expiração).
|
|
74
|
+
* @param {string|null|undefined} lid
|
|
75
|
+
* @returns {{jid: string|null, expiresAt: number, lastStoredAt: number|null}|null}
|
|
76
|
+
*/
|
|
77
|
+
const getCacheEntry = (lid) => {
|
|
78
|
+
if (!lid) return null;
|
|
79
|
+
const nowTs = now();
|
|
80
|
+
const entry = lidCache.get(lid);
|
|
81
|
+
if (entry) {
|
|
82
|
+
if (entry.expiresAt && entry.expiresAt < nowTs) {
|
|
83
|
+
lidCache.delete(lid);
|
|
84
|
+
} else {
|
|
85
|
+
return entry;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const baseLid = normalizeLid(lid);
|
|
90
|
+
if (!baseLid || baseLid === lid) return null;
|
|
91
|
+
const baseEntry = lidCache.get(baseLid);
|
|
92
|
+
if (!baseEntry) return null;
|
|
93
|
+
if (baseEntry.expiresAt && baseEntry.expiresAt < nowTs) {
|
|
94
|
+
lidCache.delete(baseLid);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return baseEntry;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Atualiza cache local do LID.
|
|
102
|
+
* @param {string} lid
|
|
103
|
+
* @param {string|null} jid
|
|
104
|
+
* @param {number} ttlMs
|
|
105
|
+
* @param {number|null} lastStoredAt
|
|
106
|
+
* @returns {void}
|
|
107
|
+
*/
|
|
108
|
+
const setCacheEntry = (lid, jid, ttlMs, lastStoredAt) => {
|
|
109
|
+
if (!lid) return;
|
|
110
|
+
const normalizedJid = normalizeWhatsAppJid(jid);
|
|
111
|
+
const baseLid = normalizeLid(lid);
|
|
112
|
+
const previousEntry = lidCache.get(lid) || (baseLid ? lidCache.get(baseLid) : null);
|
|
113
|
+
const entry = {
|
|
114
|
+
jid: normalizedJid ?? null,
|
|
115
|
+
expiresAt: now() + (ttlMs || CACHE_TTL_MS),
|
|
116
|
+
lastStoredAt: lastStoredAt ?? previousEntry?.lastStoredAt ?? null,
|
|
117
|
+
};
|
|
118
|
+
lidCache.set(lid, entry);
|
|
119
|
+
if (baseLid && baseLid !== lid) {
|
|
120
|
+
lidCache.set(baseLid, entry);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Retorna JID do cache para um LID.
|
|
126
|
+
* @param {string|null|undefined} lid
|
|
127
|
+
* @returns {string|null|undefined} undefined quando nao cacheado.
|
|
128
|
+
*/
|
|
129
|
+
export const getCachedJidForLid = (lid) => {
|
|
130
|
+
const entry = getCacheEntry(lid);
|
|
131
|
+
if (!entry) return undefined;
|
|
132
|
+
return entry.jid ?? null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Divide lista em batches.
|
|
137
|
+
* @param {Array<any>} items
|
|
138
|
+
* @param {number} [limit=BATCH_LIMIT]
|
|
139
|
+
* @returns {Array<Array<any>>}
|
|
140
|
+
*/
|
|
141
|
+
const buildChunks = (items, limit = BATCH_LIMIT) => {
|
|
142
|
+
const chunks = [];
|
|
143
|
+
for (let i = 0; i < items.length; i += limit) {
|
|
144
|
+
chunks.push(items.slice(i, i + limit));
|
|
145
|
+
}
|
|
146
|
+
return chunks;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Pré-carrega cache a partir do banco.
|
|
151
|
+
* @param {Array<string>} [lids=[]]
|
|
152
|
+
* @returns {Promise<Map<string, string|null>>}
|
|
153
|
+
*/
|
|
154
|
+
export const primeLidCache = async (lids = []) => {
|
|
155
|
+
const uniqueLids = Array.from(new Set((lids || []).filter(Boolean)));
|
|
156
|
+
if (!uniqueLids.length) return new Map();
|
|
157
|
+
|
|
158
|
+
const pending = uniqueLids.filter((lid) => isLidJid(lid) && getCachedJidForLid(lid) === undefined);
|
|
159
|
+
if (!pending.length) {
|
|
160
|
+
const map = new Map();
|
|
161
|
+
uniqueLids.forEach((lid) => map.set(lid, getCachedJidForLid(lid) ?? null));
|
|
162
|
+
return map;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const results = new Map();
|
|
166
|
+
const baseByLid = new Map();
|
|
167
|
+
const lookupSet = new Set();
|
|
168
|
+
pending.forEach((lid) => {
|
|
169
|
+
const base = normalizeLid(lid);
|
|
170
|
+
baseByLid.set(lid, base);
|
|
171
|
+
lookupSet.add(lid);
|
|
172
|
+
if (base && base !== lid) lookupSet.add(base);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const lookupList = Array.from(lookupSet);
|
|
176
|
+
const chunks = buildChunks(lookupList);
|
|
177
|
+
const rowMap = new Map();
|
|
178
|
+
|
|
179
|
+
for (const chunk of chunks) {
|
|
180
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
181
|
+
const rows = await executeQuery(`SELECT lid, jid FROM ${TABLES.LID_MAP} WHERE lid IN (${placeholders})`, chunk);
|
|
182
|
+
|
|
183
|
+
(rows || []).forEach((row) => {
|
|
184
|
+
if (!row?.lid) return;
|
|
185
|
+
const jid = row.jid && isWhatsAppJid(row.jid) ? normalizeJid(row.jid) : null;
|
|
186
|
+
rowMap.set(row.lid, jid);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const lid of pending) {
|
|
191
|
+
const base = baseByLid.get(lid);
|
|
192
|
+
const direct = rowMap.has(lid) ? rowMap.get(lid) : undefined;
|
|
193
|
+
const baseValue = base && base !== lid && rowMap.has(base) ? rowMap.get(base) : undefined;
|
|
194
|
+
const resolved = direct ?? baseValue ?? null;
|
|
195
|
+
setCacheEntry(lid, resolved, resolved ? CACHE_TTL_MS : NEGATIVE_TTL_MS);
|
|
196
|
+
results.set(lid, resolved);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
uniqueLids.forEach((lid) => {
|
|
200
|
+
if (!results.has(lid)) {
|
|
201
|
+
results.set(lid, getCachedJidForLid(lid) ?? null);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return results;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Retorna o primeiro JID valido do WhatsApp.
|
|
210
|
+
* @param {...string} candidates
|
|
211
|
+
* @returns {string|null}
|
|
212
|
+
*/
|
|
213
|
+
const pickWhatsAppJid = (...candidates) => {
|
|
214
|
+
for (const candidate of candidates) {
|
|
215
|
+
if (!candidate || typeof candidate !== 'string') continue;
|
|
216
|
+
if (isWhatsAppJid(candidate)) return normalizeJid(candidate);
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Retorna o primeiro LID valido.
|
|
223
|
+
* @param {...string} candidates
|
|
224
|
+
* @returns {string|null}
|
|
225
|
+
*/
|
|
226
|
+
const pickLid = (...candidates) => {
|
|
227
|
+
for (const candidate of candidates) {
|
|
228
|
+
if (!candidate || typeof candidate !== 'string') continue;
|
|
229
|
+
if (isLidJid(candidate)) return candidate;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const buildLidUpsertSql = (rows) => {
|
|
235
|
+
const placeholders = buildRowPlaceholders(rows, '(?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?)');
|
|
236
|
+
return `
|
|
237
|
+
INSERT INTO ${TABLES.LID_MAP} (lid, jid, first_seen, last_seen, source)
|
|
238
|
+
VALUES ${placeholders}
|
|
239
|
+
ON DUPLICATE KEY UPDATE
|
|
240
|
+
jid = COALESCE(VALUES(jid), jid),
|
|
241
|
+
last_seen = VALUES(last_seen),
|
|
242
|
+
source = VALUES(source)
|
|
243
|
+
`;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Enfileira atualizacao do lid_map (com cooldown e dedupe).
|
|
248
|
+
* @param {string} lid
|
|
249
|
+
* @param {string|null} jid
|
|
250
|
+
* @param {string} [source='message']
|
|
251
|
+
* @returns {{queued: boolean, reconciled: boolean}}
|
|
252
|
+
*/
|
|
253
|
+
export const queueLidUpdate = (lid, jid, source = 'message') => {
|
|
254
|
+
let resolvedLid = lid;
|
|
255
|
+
let resolvedJid = jid;
|
|
256
|
+
|
|
257
|
+
if (!isLidJid(resolvedLid) && isLidJid(resolvedJid) && isWhatsAppJid(resolvedLid)) {
|
|
258
|
+
resolvedLid = jid;
|
|
259
|
+
resolvedJid = lid;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!resolvedLid || !isLidJid(resolvedLid)) {
|
|
263
|
+
return { queued: false, reconciled: false };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const normalizedJid = resolvedJid && isWhatsAppJid(resolvedJid) ? normalizeJid(resolvedJid) : null;
|
|
267
|
+
const lidsToUpdate = new Set([resolvedLid]);
|
|
268
|
+
const baseLid = normalizeLid(resolvedLid);
|
|
269
|
+
if (baseLid && baseLid !== resolvedLid) lidsToUpdate.add(baseLid);
|
|
270
|
+
|
|
271
|
+
let queued = false;
|
|
272
|
+
let reconciled = false;
|
|
273
|
+
|
|
274
|
+
for (const targetLid of lidsToUpdate) {
|
|
275
|
+
const cacheEntry = getCacheEntry(targetLid);
|
|
276
|
+
const cachedJid = cacheEntry?.jid ?? null;
|
|
277
|
+
const lastStoredAt = cacheEntry?.lastStoredAt || 0;
|
|
278
|
+
const nowTs = now();
|
|
279
|
+
|
|
280
|
+
const mappingChanged = Boolean(normalizedJid && normalizedJid !== cachedJid);
|
|
281
|
+
const mappingSame = normalizedJid === cachedJid;
|
|
282
|
+
|
|
283
|
+
if (mappingSame && nowTs - lastStoredAt < STORE_COOLDOWN_MS) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const buffered = lidWriteBuffer.get(targetLid);
|
|
288
|
+
const effectiveJid = normalizedJid ?? buffered?.jid ?? cachedJid ?? null;
|
|
289
|
+
const entry = {
|
|
290
|
+
lid: targetLid,
|
|
291
|
+
jid: effectiveJid,
|
|
292
|
+
source,
|
|
293
|
+
queuedAt: nowTs,
|
|
294
|
+
reconcileJid: mappingChanged ? normalizedJid : null,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
lidWriteBuffer.set(targetLid, entry);
|
|
298
|
+
setCacheEntry(targetLid, effectiveJid, CACHE_TTL_MS, nowTs);
|
|
299
|
+
queued = true;
|
|
300
|
+
reconciled = reconciled || Boolean(entry.reconcileJid);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (queued) updateLidQueueMetric();
|
|
304
|
+
|
|
305
|
+
return { queued, reconciled };
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolve ID canônico usando apenas cache.
|
|
310
|
+
* @param {{lid?: string|null, jid?: string|null, participantAlt?: string|null}} [params]
|
|
311
|
+
* @returns {string|null}
|
|
312
|
+
*/
|
|
313
|
+
export const resolveUserIdCached = ({ lid, jid, participantAlt } = {}) => {
|
|
314
|
+
const directJid = pickWhatsAppJid(jid, participantAlt, lid);
|
|
315
|
+
if (directJid) return directJid;
|
|
316
|
+
|
|
317
|
+
const lidValue = pickLid(lid, jid, participantAlt);
|
|
318
|
+
if (!lidValue) return jid || participantAlt || lid || null;
|
|
319
|
+
|
|
320
|
+
const mapped = getCachedJidForLid(lidValue);
|
|
321
|
+
if (mapped !== undefined) return mapped || lidValue;
|
|
322
|
+
return lidValue;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Extrai informacoes do remetente a partir de uma mensagem do Baileys.
|
|
327
|
+
* @param {import('@whiskeysockets/baileys').WAMessage} msg
|
|
328
|
+
* @returns {{lid: string|null, jid: string|null, participantAlt: string|null, remoteJid: string|null, groupMessage: boolean}}
|
|
329
|
+
*/
|
|
330
|
+
export const extractSenderInfoFromMessage = (msg) => {
|
|
331
|
+
const remoteJid = msg?.key?.remoteJid || null;
|
|
332
|
+
const participant = msg?.key?.participant || null;
|
|
333
|
+
const participantAlt = msg?.key?.participantAlt || null;
|
|
334
|
+
const groupMessage = isGroupJid(remoteJid);
|
|
335
|
+
|
|
336
|
+
let lid = null;
|
|
337
|
+
let jid = null;
|
|
338
|
+
|
|
339
|
+
if (groupMessage) {
|
|
340
|
+
if (isWhatsAppJid(participant)) jid = participant;
|
|
341
|
+
if (isLidJid(participant)) lid = participant;
|
|
342
|
+
if (isWhatsAppJid(participantAlt)) jid = participantAlt;
|
|
343
|
+
if (!lid && isLidJid(participantAlt)) lid = participantAlt;
|
|
344
|
+
} else {
|
|
345
|
+
if (isWhatsAppJid(remoteJid)) jid = remoteJid;
|
|
346
|
+
if (!jid && isWhatsAppJid(participant)) jid = participant;
|
|
347
|
+
if (isLidJid(participant)) lid = participant;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { lid, jid, participantAlt, remoteJid, groupMessage };
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Busca JID para um LID no banco e atualiza cache.
|
|
355
|
+
* @param {string} lid
|
|
356
|
+
* @returns {Promise<string|null>}
|
|
357
|
+
*/
|
|
358
|
+
const fetchJidByLid = async (lid) => {
|
|
359
|
+
const cached = getCachedJidForLid(lid);
|
|
360
|
+
if (cached !== undefined) return cached || null;
|
|
361
|
+
|
|
362
|
+
const candidates = [lid];
|
|
363
|
+
const base = normalizeLid(lid);
|
|
364
|
+
if (base && base !== lid) candidates.push(base);
|
|
365
|
+
|
|
366
|
+
const placeholders = candidates.map(() => '?').join(', ');
|
|
367
|
+
const rows = await executeQuery(`SELECT lid, jid FROM ${TABLES.LID_MAP} WHERE lid IN (${placeholders})`, candidates);
|
|
368
|
+
|
|
369
|
+
const rowMap = new Map();
|
|
370
|
+
(rows || []).forEach((row) => {
|
|
371
|
+
if (!row?.lid) return;
|
|
372
|
+
const jid = row.jid && isWhatsAppJid(row.jid) ? normalizeJid(row.jid) : null;
|
|
373
|
+
rowMap.set(row.lid, jid);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const direct = rowMap.has(lid) ? rowMap.get(lid) : undefined;
|
|
377
|
+
const baseValue = base && base !== lid && rowMap.has(base) ? rowMap.get(base) : undefined;
|
|
378
|
+
let resolved = direct ?? baseValue ?? null;
|
|
379
|
+
|
|
380
|
+
if (!resolved) {
|
|
381
|
+
const normalized = base || lid;
|
|
382
|
+
const [rawUser, rawServer] = String(normalized).split('@');
|
|
383
|
+
const rootUser = rawUser ? rawUser.split(':')[0] : '';
|
|
384
|
+
const server = rawServer || '';
|
|
385
|
+
|
|
386
|
+
if (rootUser && server) {
|
|
387
|
+
const derivedRows = await executeQuery(
|
|
388
|
+
`SELECT jid
|
|
389
|
+
FROM ${TABLES.LID_MAP}
|
|
390
|
+
WHERE jid IS NOT NULL
|
|
391
|
+
AND (
|
|
392
|
+
lid = ?
|
|
393
|
+
OR lid = ?
|
|
394
|
+
OR lid = ?
|
|
395
|
+
OR lid LIKE ?
|
|
396
|
+
)
|
|
397
|
+
ORDER BY last_seen DESC
|
|
398
|
+
LIMIT 1`,
|
|
399
|
+
[lid, base || lid, `${rootUser}@${server}`, `${rootUser}:%@${server}`],
|
|
400
|
+
);
|
|
401
|
+
const derivedJid = derivedRows?.[0]?.jid;
|
|
402
|
+
if (derivedJid && isWhatsAppJid(derivedJid)) {
|
|
403
|
+
resolved = normalizeJid(derivedJid);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const shouldSeedDerived = Boolean(resolved && direct === undefined);
|
|
409
|
+
|
|
410
|
+
setCacheEntry(lid, resolved, resolved ? CACHE_TTL_MS : NEGATIVE_TTL_MS, shouldSeedDerived ? 0 : undefined);
|
|
411
|
+
|
|
412
|
+
if (shouldSeedDerived) {
|
|
413
|
+
queueLidUpdate(lid, resolved, 'derived');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return resolved;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Resolve ID canônico consultando banco se necessário.
|
|
421
|
+
* @param {{lid?: string|null, jid?: string|null, participantAlt?: string|null}} [params]
|
|
422
|
+
* @returns {Promise<string|null>}
|
|
423
|
+
*/
|
|
424
|
+
export const resolveUserId = async ({ lid, jid, participantAlt } = {}) => {
|
|
425
|
+
const directJid = pickWhatsAppJid(jid, participantAlt, lid);
|
|
426
|
+
if (directJid) return directJid;
|
|
427
|
+
|
|
428
|
+
const lidValue = pickLid(lid, jid, participantAlt);
|
|
429
|
+
if (!lidValue) return jid || participantAlt || lid || null;
|
|
430
|
+
|
|
431
|
+
const mapped = await fetchJidByLid(lidValue);
|
|
432
|
+
return mapped || lidValue;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Reconcilia mensagens antigas do LID para o JID real.
|
|
437
|
+
* @param {{lid?: string|null, jid?: string|null, source?: string}} [params]
|
|
438
|
+
* @returns {Promise<{updated: number}>}
|
|
439
|
+
*/
|
|
440
|
+
export const reconcileLidToJid = async ({ lid, jid, source = 'map' } = {}) => {
|
|
441
|
+
if (!lid || !jid) return { updated: 0 };
|
|
442
|
+
const result = await executeQuery(`UPDATE ${TABLES.MESSAGES} SET sender_id = ? WHERE sender_id = ?`, [jid, lid]);
|
|
443
|
+
const updated = Number(result?.affectedRows || 0);
|
|
444
|
+
if (updated > 0) {
|
|
445
|
+
logger.info('Reconciliação lid->jid aplicada.', {
|
|
446
|
+
lid: maskJid(lid),
|
|
447
|
+
jid: maskJid(jid),
|
|
448
|
+
updated,
|
|
449
|
+
source,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return { updated };
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const flushLidQueueCore = async () => {
|
|
456
|
+
if (lidWriteBuffer.size === 0) return;
|
|
457
|
+
const entries = Array.from(lidWriteBuffer.values());
|
|
458
|
+
for (let i = 0; i < entries.length; i += BATCH_LIMIT) {
|
|
459
|
+
const batch = entries.slice(i, i + BATCH_LIMIT);
|
|
460
|
+
if (!batch.length) continue;
|
|
461
|
+
|
|
462
|
+
const sql = buildLidUpsertSql(batch.length);
|
|
463
|
+
const params = [];
|
|
464
|
+
for (const entry of batch) {
|
|
465
|
+
params.push(entry.lid, entry.jid, entry.source);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
await executeQuery(sql, params);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
logger.error('Falha ao persistir batch do lid_map.', { error: error.message });
|
|
472
|
+
recordError('lid_map');
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const reconcileTargets = [];
|
|
477
|
+
for (const entry of batch) {
|
|
478
|
+
const current = lidWriteBuffer.get(entry.lid);
|
|
479
|
+
if (!current || current.queuedAt === entry.queuedAt) {
|
|
480
|
+
lidWriteBuffer.delete(entry.lid);
|
|
481
|
+
}
|
|
482
|
+
if (entry.reconcileJid) {
|
|
483
|
+
reconcileTargets.push({ lid: entry.lid, jid: entry.reconcileJid, source: entry.source });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
updateLidQueueMetric();
|
|
488
|
+
if (reconcileTargets.length > 0) {
|
|
489
|
+
setImmediate(() => {
|
|
490
|
+
for (const target of reconcileTargets) {
|
|
491
|
+
reconcileLidToJid(target).catch((error) => {
|
|
492
|
+
logger.warn('Falha ao reconciliar lid->jid.', { error: error.message });
|
|
493
|
+
recordError('lid_map_reconcile');
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const lidFlushRunner = createFlushRunner({
|
|
502
|
+
onFlush: flushLidQueueCore,
|
|
503
|
+
onError: (error) => {
|
|
504
|
+
logger.error('Falha ao executar flush do lid_map.', { error: error.message });
|
|
505
|
+
recordError('lid_map');
|
|
506
|
+
},
|
|
507
|
+
onFinally: () => {
|
|
508
|
+
updateLidQueueMetric();
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Executa o flush do buffer lid_map em batch.
|
|
514
|
+
* @returns {Promise<void>}
|
|
515
|
+
*/
|
|
516
|
+
export const flushLidQueue = async () => {
|
|
517
|
+
await lidFlushRunner.run();
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
export const maybeStoreLidMap = async (lid, jid, source = 'message') => {
|
|
521
|
+
const result = queueLidUpdate(lid, jid, source);
|
|
522
|
+
return { stored: result.queued, reconciled: result.reconciled };
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Extrai lid/jid/participantAlt de um objeto ou string.
|
|
527
|
+
* @param {object|string|null|undefined} value
|
|
528
|
+
* @returns {{lid: string|null, jid: string|null, participantAlt: string|null, raw: string|null}}
|
|
529
|
+
*/
|
|
530
|
+
export const extractUserIdInfo = (value) => {
|
|
531
|
+
if (!value) return { lid: null, jid: null, participantAlt: null, raw: null };
|
|
532
|
+
if (typeof value === 'string') {
|
|
533
|
+
return {
|
|
534
|
+
lid: isLidJid(value) ? value : null,
|
|
535
|
+
jid: isWhatsAppJid(value) ? value : null,
|
|
536
|
+
participantAlt: null,
|
|
537
|
+
raw: value,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const participantAlt = typeof value.participantAlt === 'string' ? value.participantAlt : null;
|
|
542
|
+
const participant = typeof value.participant === 'string' ? value.participant : null;
|
|
543
|
+
const jidCandidate = value.jid || value.id || participantAlt || participant || null;
|
|
544
|
+
const lidCandidate = value.lid || participant || null;
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
lid: pickLid(lidCandidate, participantAlt, participant),
|
|
548
|
+
jid: pickWhatsAppJid(jidCandidate, participantAlt, participant),
|
|
549
|
+
participantAlt,
|
|
550
|
+
raw: jidCandidate || lidCandidate,
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Alias: verifica se valor e LID.
|
|
556
|
+
* @param {string|null|undefined} value
|
|
557
|
+
* @returns {boolean}
|
|
558
|
+
*/
|
|
559
|
+
export const isLidUserId = (value) => isLidJid(value);
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Alias: verifica se valor e JID do WhatsApp.
|
|
563
|
+
* @param {string|null|undefined} value
|
|
564
|
+
* @returns {boolean}
|
|
565
|
+
*/
|
|
566
|
+
export const isWhatsAppUserId = (value) => isWhatsAppJid(value);
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Sleep utilitario.
|
|
570
|
+
* @param {number} ms
|
|
571
|
+
* @returns {Promise<void>}
|
|
572
|
+
*/
|
|
573
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Retorna o range de IDs da tabela messages.
|
|
577
|
+
* @returns {Promise<{minId: number, maxId: number}>}
|
|
578
|
+
*/
|
|
579
|
+
const getMessageIdRange = async () => {
|
|
580
|
+
const rows = await executeQuery(`SELECT MIN(id) AS min_id, MAX(id) AS max_id FROM ${TABLES.MESSAGES}`);
|
|
581
|
+
const minId = Number(rows?.[0]?.min_id || 0);
|
|
582
|
+
const maxId = Number(rows?.[0]?.max_id || 0);
|
|
583
|
+
return { minId, maxId };
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Executa um batch do backfill lid_map.
|
|
588
|
+
* @param {number} fromId
|
|
589
|
+
* @param {number} toId
|
|
590
|
+
* @returns {Promise<any>}
|
|
591
|
+
*/
|
|
592
|
+
const runBackfillBatch = async (fromId, toId) => {
|
|
593
|
+
const sql = `
|
|
594
|
+
INSERT INTO ${TABLES.LID_MAP} (lid, jid, first_seen, last_seen, source)
|
|
595
|
+
SELECT
|
|
596
|
+
s.lid,
|
|
597
|
+
s.jid,
|
|
598
|
+
MIN(s.ts) AS first_seen,
|
|
599
|
+
MAX(s.ts) AS last_seen,
|
|
600
|
+
?
|
|
601
|
+
FROM (
|
|
602
|
+
SELECT
|
|
603
|
+
JSON_UNQUOTE(JSON_EXTRACT(m.raw_message, '$.key.participant')) AS lid,
|
|
604
|
+
JSON_UNQUOTE(JSON_EXTRACT(m.raw_message, '$.key.participantAlt')) AS jid,
|
|
605
|
+
m.timestamp AS ts
|
|
606
|
+
FROM ${TABLES.MESSAGES} m
|
|
607
|
+
WHERE m.id BETWEEN ? AND ?
|
|
608
|
+
AND m.raw_message IS NOT NULL
|
|
609
|
+
AND m.timestamp IS NOT NULL
|
|
610
|
+
) s
|
|
611
|
+
WHERE (s.lid LIKE '%@lid' OR s.lid LIKE '%@hosted.lid')
|
|
612
|
+
AND (s.jid LIKE '%@s.whatsapp.net' OR s.jid LIKE '%@c.us' OR s.jid LIKE '%@hosted')
|
|
613
|
+
GROUP BY s.lid, s.jid
|
|
614
|
+
ON DUPLICATE KEY UPDATE
|
|
615
|
+
jid = COALESCE(VALUES(jid), ${TABLES.LID_MAP}.jid),
|
|
616
|
+
last_seen = GREATEST(${TABLES.LID_MAP}.last_seen, VALUES(last_seen)),
|
|
617
|
+
source = VALUES(source)
|
|
618
|
+
`;
|
|
619
|
+
|
|
620
|
+
return executeQuery(sql, [BACKFILL_SOURCE, fromId, toId]);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Backfill do lid_map a partir de messages.raw_message.
|
|
625
|
+
* @param {{batchSize?: number, sleepMs?: number, maxBatches?: number|null}} [options]
|
|
626
|
+
* @returns {Promise<{batches: number, minId?: number, maxId?: number}>}
|
|
627
|
+
*/
|
|
628
|
+
export const backfillLidMapFromMessages = async ({ batchSize = BACKFILL_DEFAULT_BATCH, sleepMs = 50, maxBatches = null } = {}) => {
|
|
629
|
+
const { minId, maxId } = await getMessageIdRange();
|
|
630
|
+
if (!minId || !maxId || maxId < minId) {
|
|
631
|
+
logger.info('Backfill lid_map ignorado: tabela messages vazia.');
|
|
632
|
+
return { batches: 0 };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
let batches = 0;
|
|
636
|
+
for (let start = minId; start <= maxId; start += batchSize) {
|
|
637
|
+
const end = Math.min(start + batchSize - 1, maxId);
|
|
638
|
+
await runBackfillBatch(start, end);
|
|
639
|
+
batches += 1;
|
|
640
|
+
if (maxBatches && batches >= maxBatches) break;
|
|
641
|
+
if (sleepMs > 0) await sleep(sleepMs);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
logger.info('Backfill lid_map finalizado.', { batches, minId, maxId });
|
|
645
|
+
return { batches, minId, maxId };
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Garante que o backfill rode apenas uma vez por processo.
|
|
650
|
+
* @param {{batchSize?: number, sleepMs?: number, maxBatches?: number|null}} [options]
|
|
651
|
+
* @returns {Promise<{batches: number, minId?: number, maxId?: number}>}
|
|
652
|
+
*/
|
|
653
|
+
export const backfillLidMapFromMessagesOnce = async (options = {}) => {
|
|
654
|
+
if (!backfillPromise) {
|
|
655
|
+
backfillPromise = backfillLidMapFromMessages(options).catch((error) => {
|
|
656
|
+
logger.warn('Falha no backfill lid_map.', { error: error.message });
|
|
657
|
+
throw error;
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return backfillPromise;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
updateLidQueueMetric();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logger from '../utils/logger/loggerModule.js';
|
|
2
|
+
import { queueMessageInsert } from './dbWriteQueue.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converte um timestamp da mensagem para ms com fallback seguro.
|
|
6
|
+
* @param {import('@whiskeysockets/baileys').WAMessage} msg
|
|
7
|
+
* @returns {number}
|
|
8
|
+
*/
|
|
9
|
+
const resolveMessageTimestampMs = (msg) => {
|
|
10
|
+
const rawTimestamp = msg?.messageTimestamp;
|
|
11
|
+
if (rawTimestamp !== null && rawTimestamp !== undefined) {
|
|
12
|
+
const tsNumber = typeof rawTimestamp === 'number' ? rawTimestamp : Number(rawTimestamp);
|
|
13
|
+
if (Number.isFinite(tsNumber) && tsNumber > 0) {
|
|
14
|
+
return tsNumber * 1000;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return Date.now();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normaliza uma mensagem do Baileys para o formato persistido no banco.
|
|
22
|
+
* @param {import('@whiskeysockets/baileys').WAMessage} msg - Mensagem recebida/enviada.
|
|
23
|
+
* @param {string} [senderId] - ID do remetente (opcional).
|
|
24
|
+
* @returns {Object} Objeto com dados prontos para persistencia.
|
|
25
|
+
*/
|
|
26
|
+
export const buildMessageData = (msg, senderId) => ({
|
|
27
|
+
message_id: msg?.key?.id,
|
|
28
|
+
chat_id: msg?.key?.remoteJid,
|
|
29
|
+
sender_id: senderId || msg?.key?.participant || msg?.key?.remoteJid,
|
|
30
|
+
content: msg?.message?.conversation || msg?.message?.extendedTextMessage?.text || null,
|
|
31
|
+
raw_message: msg || {},
|
|
32
|
+
timestamp: new Date(resolveMessageTimestampMs(msg)),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Envia uma mensagem via Baileys e persiste imediatamente o retorno.
|
|
37
|
+
* @param {import('@whiskeysockets/baileys').WASocket} sock
|
|
38
|
+
* @param {string} jid
|
|
39
|
+
* @param {Object} content
|
|
40
|
+
* @param {Object} [options]
|
|
41
|
+
* @returns {Promise<import('@whiskeysockets/baileys').WAMessage>}
|
|
42
|
+
*/
|
|
43
|
+
export async function sendAndStore(sock, jid, content, options) {
|
|
44
|
+
const sent = await sock.sendMessage(jid, content, options);
|
|
45
|
+
const senderId = sock?.user?.id || sent?.key?.participant;
|
|
46
|
+
try {
|
|
47
|
+
queueMessageInsert(buildMessageData(sent, senderId));
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.warn('Falha ao enfileirar mensagem enviada para persistencia.', {
|
|
50
|
+
error: error.message,
|
|
51
|
+
messageId: sent?.key?.id,
|
|
52
|
+
remoteJid: sent?.key?.remoteJid,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return sent;
|
|
56
|
+
}
|