@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,572 @@
|
|
|
1
|
+
import logger from '../utils/logger/loggerModule.js';
|
|
2
|
+
import { executeQuery, TABLES } from '../../database/index.js';
|
|
3
|
+
import { queueLidUpdate, flushLidQueue } from './lidMapService.js';
|
|
4
|
+
import { buildPlaceholders, createFlushRunner } from './queueUtils.js';
|
|
5
|
+
import { recordError, setQueueDepth } from '../observability/metrics.js';
|
|
6
|
+
import { sanitizeUnicodeString, toSafeJsonColumnValue } from '../utils/json/jsonSanitizer.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converte um valor para número com fallback.
|
|
10
|
+
*
|
|
11
|
+
* @param {*} value - Valor de entrada (string, number, etc.).
|
|
12
|
+
* @param {number} fallback - Valor padrão caso a conversão falhe.
|
|
13
|
+
* @returns {number} Número finito convertido, ou o fallback.
|
|
14
|
+
*/
|
|
15
|
+
const parseNumber = (value, fallback) => {
|
|
16
|
+
const parsed = Number(value);
|
|
17
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Intervalo (ms) do flush periódico das filas de escrita no banco.
|
|
22
|
+
* É limitado entre 1000ms e 3000ms por segurança.
|
|
23
|
+
*
|
|
24
|
+
* @type {number}
|
|
25
|
+
*/
|
|
26
|
+
const FLUSH_INTERVAL_MS = Math.min(
|
|
27
|
+
3000,
|
|
28
|
+
Math.max(1000, parseNumber(process.env.DB_WRITE_FLUSH_MS, 1500)),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tamanho máximo do batch de mensagens por INSERT.
|
|
33
|
+
*
|
|
34
|
+
* @type {number}
|
|
35
|
+
*/
|
|
36
|
+
const MESSAGE_BATCH_SIZE = Math.max(1, Math.floor(parseNumber(process.env.DB_MESSAGE_BATCH_SIZE, 200)));
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Tamanho máximo do batch de chats por INSERT/UPSERT.
|
|
40
|
+
*
|
|
41
|
+
* @type {number}
|
|
42
|
+
*/
|
|
43
|
+
const CHAT_BATCH_SIZE = Math.max(1, Math.floor(parseNumber(process.env.DB_CHAT_BATCH_SIZE, 200)));
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Tempo de “cooldown” (ms) por chat antes de permitir novo write no banco.
|
|
47
|
+
* Ajuda a reduzir writes repetidos do mesmo chat em curto período.
|
|
48
|
+
*
|
|
49
|
+
* @type {number}
|
|
50
|
+
*/
|
|
51
|
+
const CHAT_COOLDOWN_MS = Math.max(1000, Math.floor(parseNumber(process.env.DB_CHAT_COOLDOWN_MS, 45000)));
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Capacidade máxima da fila de mensagens.
|
|
55
|
+
* Quando atinge esse limite, forçamos flush para tentar esvaziar.
|
|
56
|
+
*
|
|
57
|
+
* @type {number}
|
|
58
|
+
*/
|
|
59
|
+
const MESSAGE_QUEUE_MAX = Math.max(
|
|
60
|
+
MESSAGE_BATCH_SIZE * 5,
|
|
61
|
+
Math.floor(parseNumber(process.env.DB_MESSAGE_QUEUE_MAX, 5000)),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Regex de erro para payload JSON inválido no MySQL.
|
|
66
|
+
* @type {RegExp}
|
|
67
|
+
*/
|
|
68
|
+
const INVALID_JSON_TEXT_REGEX = /Invalid JSON text/i;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Regex específica para surrogate inválido.
|
|
72
|
+
* @type {RegExp}
|
|
73
|
+
*/
|
|
74
|
+
const INVALID_SURROGATE_REGEX = /surrogate pair/i;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fila em memória com mensagens pendentes de persistência.
|
|
78
|
+
* @type {Array<Object>}
|
|
79
|
+
*/
|
|
80
|
+
const messageQueue = [];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Conjunto de IDs de mensagens que já estão enfileiradas, para evitar duplicação.
|
|
84
|
+
* @type {Set<string>}
|
|
85
|
+
*/
|
|
86
|
+
const messagePendingIds = new Set();
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Fila (por chat.id) com atualizações pendentes de chats.
|
|
91
|
+
* @type {Map<string, {id:string, name:(string|null), raw:(Object|null), rawHash:string, queuedAt:number, nextAllowedAt:number}>}
|
|
92
|
+
*/
|
|
93
|
+
const chatQueue = new Map();
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Cache por chat.id para comparar estado persistido vs pendente (hash/name/último write).
|
|
97
|
+
* @type {Map<string, {
|
|
98
|
+
* storedRaw: (Object|null),
|
|
99
|
+
* storedHash: (string|null),
|
|
100
|
+
* storedName: (string|null),
|
|
101
|
+
* lastWriteAt: number,
|
|
102
|
+
* pendingRaw: (Object|null),
|
|
103
|
+
* pendingHash: (string|null),
|
|
104
|
+
* pendingName: (string|null)
|
|
105
|
+
* }>}
|
|
106
|
+
*/
|
|
107
|
+
const chatCache = new Map();
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Indica se já há um flush agendado via setImmediate.
|
|
112
|
+
* @type {boolean}
|
|
113
|
+
*/
|
|
114
|
+
let flushScheduled = false;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Atualiza as métricas de profundidade das filas (monitoramento).
|
|
118
|
+
*
|
|
119
|
+
* @returns {void}
|
|
120
|
+
*/
|
|
121
|
+
const updateQueueMetrics = () => {
|
|
122
|
+
setQueueDepth('messages', messageQueue.length);
|
|
123
|
+
setQueueDepth('chats', chatQueue.size);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Agenda a execução de flush das filas no próximo ciclo do event-loop.
|
|
128
|
+
* Evita agendar múltiplas execuções repetidas em sequência.
|
|
129
|
+
*
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
132
|
+
const scheduleFlush = () => {
|
|
133
|
+
if (flushScheduled) return;
|
|
134
|
+
flushScheduled = true;
|
|
135
|
+
setImmediate(() => {
|
|
136
|
+
flushScheduled = false;
|
|
137
|
+
flushQueues().catch((error) => {
|
|
138
|
+
logger.error('Falha ao executar flush das filas.', { error: error.message });
|
|
139
|
+
recordError('db_write_queue');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Calcula um hash FNV-1a (32-bit) de uma string.
|
|
146
|
+
* Útil para detectar mudanças em objetos serializados.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} input - Texto de entrada.
|
|
149
|
+
* @returns {string} Hash em hexadecimal (8 chars).
|
|
150
|
+
*/
|
|
151
|
+
const fnv1aHash = (input) => {
|
|
152
|
+
let hash = 0x811c9dc5;
|
|
153
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
154
|
+
hash ^= input.charCodeAt(i);
|
|
155
|
+
hash = (hash + (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)) >>> 0;
|
|
156
|
+
}
|
|
157
|
+
return hash.toString(16).padStart(8, '0');
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Serializa um valor para string de forma estável (ordena chaves),
|
|
162
|
+
* com proteção contra referências circulares e profundidade máxima.
|
|
163
|
+
*
|
|
164
|
+
* @param {*} value - Valor a serializar.
|
|
165
|
+
* @param {number} [depth=0] - Profundidade atual (uso interno).
|
|
166
|
+
* @param {WeakSet<object>} [seen=new WeakSet()] - Rastreamento de objetos vistos (circular).
|
|
167
|
+
* @returns {string} String estável do valor.
|
|
168
|
+
*/
|
|
169
|
+
const stableStringify = (value, depth = 0, seen = new WeakSet()) => {
|
|
170
|
+
if (value === null || value === undefined) return 'null';
|
|
171
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
172
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
173
|
+
if (typeof value === 'bigint') return `"${value.toString()}"`;
|
|
174
|
+
if (value instanceof Date) return `"${value.toISOString()}"`;
|
|
175
|
+
if (Buffer.isBuffer(value)) return `"Buffer:${value.length}"`;
|
|
176
|
+
if (typeof value !== 'object') return `"${String(value)}"`;
|
|
177
|
+
if (seen.has(value)) return '"[Circular]"';
|
|
178
|
+
if (depth > 6) return '"[MaxDepth]"';
|
|
179
|
+
|
|
180
|
+
seen.add(value);
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
const items = value.map((item) => stableStringify(item, depth + 1, seen));
|
|
183
|
+
return `[${items.join(',')}]`;
|
|
184
|
+
}
|
|
185
|
+
const keys = Object.keys(value).sort();
|
|
186
|
+
const items = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key], depth + 1, seen)}`);
|
|
187
|
+
return `{${items.join(',')}}`;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Gera um hash estável de qualquer valor/objeto.
|
|
192
|
+
* Caso a serialização falhe, usa fallback para String(value).
|
|
193
|
+
*
|
|
194
|
+
* @param {*} value - Valor a hashear.
|
|
195
|
+
* @returns {string} Hash em hexadecimal (8 chars).
|
|
196
|
+
*/
|
|
197
|
+
const hashObject = (value) => {
|
|
198
|
+
try {
|
|
199
|
+
return fnv1aHash(stableStringify(value));
|
|
200
|
+
} catch {
|
|
201
|
+
return fnv1aHash(String(value));
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Indica se o erro foi causado por texto JSON inválido (payload determinístico).
|
|
207
|
+
*
|
|
208
|
+
* @param {Error} error
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
const isInvalidJsonPayloadError = (error) => {
|
|
212
|
+
const message = error?.message || '';
|
|
213
|
+
return INVALID_JSON_TEXT_REGEX.test(message) || INVALID_SURROGATE_REGEX.test(message);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Normaliza payload de mensagem antes de persistir.
|
|
218
|
+
* - content: remove surrogate inválido
|
|
219
|
+
* - raw_message: serializa JSON seguro para coluna JSON
|
|
220
|
+
*
|
|
221
|
+
* @param {{message_id:string, chat_id:string, sender_id:string, content:(string|null), raw_message:(Object|string|null), timestamp:(number|string|Date)}} messageData
|
|
222
|
+
* @returns {{message_id:string, chat_id:string, sender_id:string, content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}}
|
|
223
|
+
*/
|
|
224
|
+
const normalizeMessageForQueue = (messageData) => ({
|
|
225
|
+
...messageData,
|
|
226
|
+
content: typeof messageData?.content === 'string' ? sanitizeUnicodeString(messageData.content) : messageData?.content,
|
|
227
|
+
raw_message: toSafeJsonColumnValue(messageData?.raw_message),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Executa INSERT IGNORE de um batch de mensagens.
|
|
232
|
+
*
|
|
233
|
+
* @param {Array<{message_id:string, chat_id:string, sender_id:string, content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
|
|
234
|
+
* @returns {Promise<void>}
|
|
235
|
+
*/
|
|
236
|
+
const insertMessageBatch = async (batch) => {
|
|
237
|
+
const placeholders = buildPlaceholders(batch.length, 6);
|
|
238
|
+
const params = [];
|
|
239
|
+
for (const message of batch) {
|
|
240
|
+
params.push(
|
|
241
|
+
message.message_id,
|
|
242
|
+
message.chat_id,
|
|
243
|
+
message.sender_id,
|
|
244
|
+
message.content,
|
|
245
|
+
message.raw_message,
|
|
246
|
+
message.timestamp,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const sql = `INSERT IGNORE INTO ${TABLES.MESSAGES}
|
|
251
|
+
(message_id, chat_id, sender_id, content, raw_message, timestamp)
|
|
252
|
+
VALUES ${placeholders}`;
|
|
253
|
+
|
|
254
|
+
await executeQuery(sql, params);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Remove IDs de mensagens do set de pendentes.
|
|
259
|
+
*
|
|
260
|
+
* @param {Array<{message_id:string}>} batch
|
|
261
|
+
* @returns {void}
|
|
262
|
+
*/
|
|
263
|
+
const clearPendingMessageIds = (batch) => {
|
|
264
|
+
for (const message of batch) {
|
|
265
|
+
messagePendingIds.delete(message.message_id);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Em caso de erro JSON no batch, tenta persistir item a item.
|
|
271
|
+
* - Mensagem inválida é descartada para não travar a fila inteira.
|
|
272
|
+
* - Em erro transitório, re-enfileira o restante e interrompe.
|
|
273
|
+
*
|
|
274
|
+
* @param {Array<{message_id:string, chat_id:string, sender_id:string, content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
|
|
275
|
+
* @returns {Promise<void>}
|
|
276
|
+
*/
|
|
277
|
+
const salvageJsonErrorBatch = async (batch) => {
|
|
278
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
279
|
+
const message = batch[index];
|
|
280
|
+
try {
|
|
281
|
+
await insertMessageBatch([message]);
|
|
282
|
+
clearPendingMessageIds([message]);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (isInvalidJsonPayloadError(error)) {
|
|
285
|
+
clearPendingMessageIds([message]);
|
|
286
|
+
logger.warn('Mensagem descartada por payload JSON inválido.', {
|
|
287
|
+
messageId: message?.message_id,
|
|
288
|
+
chatId: message?.chat_id,
|
|
289
|
+
error: error.message,
|
|
290
|
+
});
|
|
291
|
+
recordError('db_write_queue');
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
messageQueue.unshift(...batch.slice(index));
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const flushMessageQueueCore = async () => {
|
|
302
|
+
while (messageQueue.length > 0) {
|
|
303
|
+
const batch = messageQueue.splice(0, MESSAGE_BATCH_SIZE);
|
|
304
|
+
try {
|
|
305
|
+
await insertMessageBatch(batch);
|
|
306
|
+
clearPendingMessageIds(batch);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
logger.error('Falha ao inserir batch de mensagens.', { error: error.message });
|
|
309
|
+
recordError('db_write_queue');
|
|
310
|
+
|
|
311
|
+
if (isInvalidJsonPayloadError(error)) {
|
|
312
|
+
try {
|
|
313
|
+
await salvageJsonErrorBatch(batch);
|
|
314
|
+
continue;
|
|
315
|
+
} catch (salvageError) {
|
|
316
|
+
logger.error('Falha ao recuperar batch de mensagens após erro de JSON inválido.', {
|
|
317
|
+
error: salvageError.message,
|
|
318
|
+
});
|
|
319
|
+
recordError('db_write_queue');
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
messageQueue.unshift(...batch);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const flushChatQueueCore = async () => {
|
|
331
|
+
while (chatQueue.size > 0) {
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
const ready = [];
|
|
334
|
+
for (const entry of chatQueue.values()) {
|
|
335
|
+
if (now < entry.nextAllowedAt) continue;
|
|
336
|
+
ready.push(entry);
|
|
337
|
+
if (ready.length >= CHAT_BATCH_SIZE) break;
|
|
338
|
+
}
|
|
339
|
+
if (!ready.length) break;
|
|
340
|
+
|
|
341
|
+
const placeholders = buildPlaceholders(ready.length, 3);
|
|
342
|
+
const params = [];
|
|
343
|
+
for (const entry of ready) {
|
|
344
|
+
if (typeof entry.name === 'string') {
|
|
345
|
+
entry.name = sanitizeUnicodeString(entry.name);
|
|
346
|
+
}
|
|
347
|
+
params.push(entry.id, entry.name, toSafeJsonColumnValue(entry.raw));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const sql = `INSERT INTO ${TABLES.CHATS} (id, name, raw_chat)
|
|
351
|
+
VALUES ${placeholders}
|
|
352
|
+
ON DUPLICATE KEY UPDATE
|
|
353
|
+
name = COALESCE(VALUES(name), name),
|
|
354
|
+
raw_chat = COALESCE(VALUES(raw_chat), raw_chat)`;
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
await executeQuery(sql, params);
|
|
358
|
+
const writeAt = Date.now();
|
|
359
|
+
for (const entry of ready) {
|
|
360
|
+
const current = chatQueue.get(entry.id);
|
|
361
|
+
const cache = chatCache.get(entry.id) || {};
|
|
362
|
+
|
|
363
|
+
if (entry.raw) {
|
|
364
|
+
cache.storedRaw = entry.raw;
|
|
365
|
+
cache.storedHash = entry.rawHash;
|
|
366
|
+
}
|
|
367
|
+
if (entry.name !== null && entry.name !== undefined) {
|
|
368
|
+
cache.storedName = entry.name;
|
|
369
|
+
}
|
|
370
|
+
cache.lastWriteAt = writeAt;
|
|
371
|
+
|
|
372
|
+
if (!current || current.queuedAt === entry.queuedAt) {
|
|
373
|
+
chatQueue.delete(entry.id);
|
|
374
|
+
cache.pendingRaw = null;
|
|
375
|
+
cache.pendingHash = null;
|
|
376
|
+
cache.pendingName = null;
|
|
377
|
+
} else {
|
|
378
|
+
current.nextAllowedAt = writeAt + CHAT_COOLDOWN_MS;
|
|
379
|
+
chatQueue.set(entry.id, current);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
chatCache.set(entry.id, cache);
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
logger.error('Falha ao inserir batch de chats.', { error: error.message });
|
|
386
|
+
recordError('db_write_queue');
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const messageFlushRunner = createFlushRunner({
|
|
393
|
+
onFlush: flushMessageQueueCore,
|
|
394
|
+
onError: (error) => {
|
|
395
|
+
logger.error('Falha ao executar flush da fila de mensagens.', { error: error.message });
|
|
396
|
+
recordError('db_write_queue');
|
|
397
|
+
},
|
|
398
|
+
onFinally: () => {
|
|
399
|
+
updateQueueMetrics();
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const chatFlushRunner = createFlushRunner({
|
|
404
|
+
onFlush: flushChatQueueCore,
|
|
405
|
+
onError: (error) => {
|
|
406
|
+
logger.error('Falha ao executar flush da fila de chats.', { error: error.message });
|
|
407
|
+
recordError('db_write_queue');
|
|
408
|
+
},
|
|
409
|
+
onFinally: () => {
|
|
410
|
+
updateQueueMetrics();
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Enfileira uma mensagem para INSERT no banco (INSERT IGNORE).
|
|
416
|
+
* - Evita duplicar message_id usando um Set.
|
|
417
|
+
* - Força flush se a fila estiver muito grande.
|
|
418
|
+
* - Agenda flush quando atinge o tamanho de batch.
|
|
419
|
+
*
|
|
420
|
+
* @param {{message_id:string, chat_id:string, sender_id:string, content:(string|null), raw_message:(Object|string|null), timestamp:(number|string)}} messageData
|
|
421
|
+
* Objeto com os campos necessários para persistência.
|
|
422
|
+
* @returns {boolean} true se foi enfileirada; false se inválida/duplicada.
|
|
423
|
+
*/
|
|
424
|
+
export function queueMessageInsert(messageData) {
|
|
425
|
+
if (!messageData?.message_id) return false;
|
|
426
|
+
if (messagePendingIds.has(messageData.message_id)) return false;
|
|
427
|
+
|
|
428
|
+
const normalizedMessage = normalizeMessageForQueue(messageData);
|
|
429
|
+
|
|
430
|
+
if (messageQueue.length >= MESSAGE_QUEUE_MAX) {
|
|
431
|
+
logger.warn('Fila de mensagens cheia, forçando flush.', { size: messageQueue.length });
|
|
432
|
+
scheduleFlush();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
messagePendingIds.add(normalizedMessage.message_id);
|
|
436
|
+
messageQueue.push(normalizedMessage);
|
|
437
|
+
updateQueueMetrics();
|
|
438
|
+
|
|
439
|
+
if (messageQueue.length >= MESSAGE_BATCH_SIZE) {
|
|
440
|
+
scheduleFlush();
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Enfileira uma atualização de chat para UPSERT no banco.
|
|
447
|
+
* Suporta atualização parcial (merge com cache) e opção de “forçar nome”.
|
|
448
|
+
*
|
|
449
|
+
* Regras:
|
|
450
|
+
* - Detecta mudança de "raw_chat" via hash estável.
|
|
451
|
+
* - Detecta mudança de "name" apenas quando fornecido.
|
|
452
|
+
* - Aplica cooldown por chat para reduzir writes repetidos.
|
|
453
|
+
*
|
|
454
|
+
* @param {{id:string, name?:string, [key:string]:any}} chat - Objeto do chat (mínimo: {id}).
|
|
455
|
+
* @param {{partial?:boolean, forceName?:boolean}} [options={}]
|
|
456
|
+
* partial: se true, faz merge do chat com o estado base do cache.
|
|
457
|
+
* forceName: se true, sempre tenta gravar name (fallback para id).
|
|
458
|
+
* @returns {boolean} true se algo mudou e foi enfileirado; false se nada mudou ou inválido.
|
|
459
|
+
*/
|
|
460
|
+
export function queueChatUpdate(chat, options = {}) {
|
|
461
|
+
if (!chat || !chat.id) return false;
|
|
462
|
+
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
const isPartial = Boolean(options.partial);
|
|
465
|
+
const forceName = Boolean(options.forceName);
|
|
466
|
+
const cache = chatCache.get(chat.id) || {
|
|
467
|
+
storedRaw: null,
|
|
468
|
+
storedHash: null,
|
|
469
|
+
storedName: null,
|
|
470
|
+
lastWriteAt: 0,
|
|
471
|
+
pendingRaw: null,
|
|
472
|
+
pendingHash: null,
|
|
473
|
+
pendingName: null,
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const baseRaw = cache.pendingRaw || cache.storedRaw;
|
|
477
|
+
const rawChat = isPartial ? (baseRaw ? { ...baseRaw, ...chat } : null) : chat;
|
|
478
|
+
const rawHash = rawChat ? hashObject(rawChat) : cache.pendingHash || cache.storedHash;
|
|
479
|
+
|
|
480
|
+
const providedName = forceName ? chat.name || chat.id : chat.name;
|
|
481
|
+
const nameProvided = providedName !== undefined && providedName !== null;
|
|
482
|
+
const name = nameProvided ? providedName : cache.pendingName || cache.storedName || null;
|
|
483
|
+
const compareHash = cache.pendingHash || cache.storedHash;
|
|
484
|
+
const compareName = cache.pendingName || cache.storedName;
|
|
485
|
+
|
|
486
|
+
const rawChanged = Boolean(rawChat && rawHash && rawHash !== compareHash);
|
|
487
|
+
const nameChanged = nameProvided && name !== compareName;
|
|
488
|
+
|
|
489
|
+
if (!rawChanged && !nameChanged) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const nextAllowedAt = cache.lastWriteAt ? cache.lastWriteAt + CHAT_COOLDOWN_MS : now;
|
|
494
|
+
const entry = {
|
|
495
|
+
id: chat.id,
|
|
496
|
+
name: nameProvided ? name : null,
|
|
497
|
+
raw: rawChanged ? rawChat : null,
|
|
498
|
+
rawHash: rawHash || compareHash,
|
|
499
|
+
queuedAt: now,
|
|
500
|
+
nextAllowedAt,
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
chatQueue.set(chat.id, entry);
|
|
504
|
+
chatCache.set(chat.id, {
|
|
505
|
+
...cache,
|
|
506
|
+
pendingRaw: rawChat || cache.pendingRaw,
|
|
507
|
+
pendingHash: rawHash || cache.pendingHash,
|
|
508
|
+
pendingName: nameProvided ? name : cache.pendingName,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
updateQueueMetrics();
|
|
512
|
+
scheduleFlush();
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Faz flush da fila de mensagens:
|
|
518
|
+
* - Processa em batches (MESSAGE_BATCH_SIZE).
|
|
519
|
+
* - Usa INSERT IGNORE para evitar duplicidade.
|
|
520
|
+
* - Em erro, re-enfileira o batch no início e interrompe para tentar depois.
|
|
521
|
+
*
|
|
522
|
+
* @returns {Promise<void>}
|
|
523
|
+
*/
|
|
524
|
+
async function flushMessageQueue() {
|
|
525
|
+
await messageFlushRunner.run();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Faz flush da fila de chats:
|
|
530
|
+
* - Respeita cooldown (nextAllowedAt) por chat.
|
|
531
|
+
* - Processa em batches (CHAT_BATCH_SIZE).
|
|
532
|
+
* - Usa UPSERT para atualizar name/raw_chat quando fornecidos.
|
|
533
|
+
*
|
|
534
|
+
* @returns {Promise<void>}
|
|
535
|
+
*/
|
|
536
|
+
async function flushChatQueue() {
|
|
537
|
+
await chatFlushRunner.run();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Executa flush de todas as filas (mensagens, chats e LID).
|
|
542
|
+
* Usa Promise.allSettled para não “matar” as outras filas caso uma falhe.
|
|
543
|
+
*
|
|
544
|
+
* @returns {Promise<void>}
|
|
545
|
+
*/
|
|
546
|
+
export async function flushQueues() {
|
|
547
|
+
await Promise.allSettled([flushMessageQueue(), flushChatQueue(), flushLidQueue()]);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
updateQueueMetrics();
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Timer periódico para garantir flush mesmo sem eventos/tráfego.
|
|
554
|
+
* O timer é "unref" quando disponível, para não segurar o processo aberto.
|
|
555
|
+
*/
|
|
556
|
+
if (FLUSH_INTERVAL_MS > 0) {
|
|
557
|
+
const timer = setInterval(() => {
|
|
558
|
+
flushQueues().catch((error) => {
|
|
559
|
+
logger.error('Erro ao executar flush periódico das filas.', { error: error.message });
|
|
560
|
+
recordError('db_write_queue');
|
|
561
|
+
});
|
|
562
|
+
}, FLUSH_INTERVAL_MS);
|
|
563
|
+
if (typeof timer.unref === 'function') {
|
|
564
|
+
timer.unref();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Re-export do enfileiramento de update de LID.
|
|
570
|
+
* @type {(update:any)=>any}
|
|
571
|
+
*/
|
|
572
|
+
export { queueLidUpdate };
|