@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.
Files changed (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. 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
+ }