@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,239 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
6
|
+
import { getJidUser } from '../../config/baileysConfig.js';
|
|
7
|
+
|
|
8
|
+
const TEMP_DIR = path.join(process.cwd(), 'temp', 'stickers');
|
|
9
|
+
const METADATA_MAX_LENGTH = 64;
|
|
10
|
+
const WEBPMUX_CHECK_TIMEOUT_MS = 5000;
|
|
11
|
+
const WEBPMUX_EXEC_TIMEOUT_MS = 12000;
|
|
12
|
+
|
|
13
|
+
let webpmuxAvailabilityPromise = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Contexto de placeholders usados nos metadados (`#nome`, `#id`).
|
|
17
|
+
* @typedef {Object} StickerReplaceContext
|
|
18
|
+
* @property {string} [senderName] - Nome exibido do remetente.
|
|
19
|
+
* @property {string} [userId] - JID/ID de origem para preencher `#id`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resultado da execução de processo externo.
|
|
24
|
+
* @typedef {Object} ProcessExecutionResult
|
|
25
|
+
* @property {string} stdout
|
|
26
|
+
* @property {string} stderr
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Envia sinal para um processo filho com tratamento seguro de erro.
|
|
31
|
+
*
|
|
32
|
+
* @param {import('node:child_process').ChildProcessWithoutNullStreams} child - Processo filho alvo.
|
|
33
|
+
* @param {NodeJS.Signals} signal - Sinal a ser enviado.
|
|
34
|
+
* @returns {boolean} `true` se o sinal foi enviado; `false` caso contrário.
|
|
35
|
+
*/
|
|
36
|
+
function safeKill(child, signal) {
|
|
37
|
+
try {
|
|
38
|
+
return child.kill(signal);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Executa um comando externo com timeout e captura de stdout/stderr.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} command - Binário a executar.
|
|
48
|
+
* @param {string[]} args - Argumentos separados (sem shell).
|
|
49
|
+
* @param {{ timeoutMs: number }} options - Opções de execução.
|
|
50
|
+
* @returns {Promise<ProcessExecutionResult>} Saídas do comando.
|
|
51
|
+
*/
|
|
52
|
+
function runProcess(command, args, { timeoutMs }) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
55
|
+
let stdout = '';
|
|
56
|
+
let stderr = '';
|
|
57
|
+
let didTimeout = false;
|
|
58
|
+
|
|
59
|
+
const timeoutRef = setTimeout(() => {
|
|
60
|
+
didTimeout = true;
|
|
61
|
+
safeKill(child, 'SIGTERM');
|
|
62
|
+
setTimeout(() => safeKill(child, 'SIGKILL'), 1500);
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
|
|
65
|
+
child.stdout.on('data', (chunk) => {
|
|
66
|
+
stdout += chunk.toString();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
child.stderr.on('data', (chunk) => {
|
|
70
|
+
stderr += chunk.toString();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
child.on('error', (error) => {
|
|
74
|
+
clearTimeout(timeoutRef);
|
|
75
|
+
reject(error);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
child.on('close', (code, signal) => {
|
|
79
|
+
clearTimeout(timeoutRef);
|
|
80
|
+
|
|
81
|
+
if (didTimeout) {
|
|
82
|
+
const timeoutError = new Error(`Processo excedeu o timeout de ${timeoutMs}ms.`);
|
|
83
|
+
timeoutError.code = 'ETIMEDOUT';
|
|
84
|
+
timeoutError.signal = signal;
|
|
85
|
+
timeoutError.stderr = stderr;
|
|
86
|
+
reject(timeoutError);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (code !== 0) {
|
|
91
|
+
const processError = new Error(`${command} finalizou com código ${code}${signal ? ` (sinal: ${signal})` : ''}.`);
|
|
92
|
+
processError.code = code;
|
|
93
|
+
processError.signal = signal;
|
|
94
|
+
processError.stderr = stderr;
|
|
95
|
+
processError.stdout = stdout;
|
|
96
|
+
reject(processError);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
resolve({ stdout, stderr });
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Verifica se `webpmux` está disponível no ambiente, com cache em memória.
|
|
107
|
+
*
|
|
108
|
+
* @returns {Promise<true>} Resolve `true` quando o binário está disponível.
|
|
109
|
+
* @throws {Error} Quando `webpmux` não está instalado ou não responde.
|
|
110
|
+
*/
|
|
111
|
+
async function ensureWebpmuxAvailable() {
|
|
112
|
+
if (webpmuxAvailabilityPromise) {
|
|
113
|
+
return webpmuxAvailabilityPromise;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
webpmuxAvailabilityPromise = runProcess('webpmux', ['-version'], {
|
|
117
|
+
timeoutMs: WEBPMUX_CHECK_TIMEOUT_MS,
|
|
118
|
+
})
|
|
119
|
+
.then(() => true)
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
webpmuxAvailabilityPromise = null;
|
|
122
|
+
throw error;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return webpmuxAvailabilityPromise;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Normaliza texto de metadado: remove quebras de linha, compacta espaços e limita tamanho.
|
|
130
|
+
*
|
|
131
|
+
* @param {unknown} value - Valor de entrada.
|
|
132
|
+
* @param {string} [fallback=''] - Valor padrão quando o resultado fica vazio.
|
|
133
|
+
* @returns {string} Texto pronto para ser serializado no EXIF.
|
|
134
|
+
*/
|
|
135
|
+
function normalizeMetadataText(value, fallback = '') {
|
|
136
|
+
const normalized = String(value ?? '')
|
|
137
|
+
.replace(/[\r\n]+/g, ' ')
|
|
138
|
+
.replace(/\s+/g, ' ')
|
|
139
|
+
.trim()
|
|
140
|
+
.slice(0, METADATA_MAX_LENGTH);
|
|
141
|
+
return normalized || fallback;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Adiciona metadados EXIF de pacote ao arquivo WEBP.
|
|
146
|
+
*
|
|
147
|
+
* Em caso de erro, a função retorna o `stickerPath` original para evitar bloquear o envio.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} stickerPath - Caminho do WEBP base.
|
|
150
|
+
* @param {string} packName - Nome do pacote (aceita placeholders: `#nome`, `#data`, `#hora`, `#id`).
|
|
151
|
+
* @param {string} packAuthor - Autor do pacote (aceita placeholders: `#nome`, `#data`, `#hora`, `#id`).
|
|
152
|
+
* @param {StickerReplaceContext} [replaceContext={}] - Dados para substituir placeholders.
|
|
153
|
+
* @returns {Promise<string>} Caminho do WEBP final com metadados (ou o original em fallback).
|
|
154
|
+
*/
|
|
155
|
+
export async function addStickerMetadata(stickerPath, packName, packAuthor, replaceContext = {}) {
|
|
156
|
+
const { senderName = '', userId = '' } = replaceContext;
|
|
157
|
+
const now = new Date();
|
|
158
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
159
|
+
const dataAtual = `${pad(now.getDate())}/${pad(now.getMonth() + 1)}/${now.getFullYear()}`;
|
|
160
|
+
const horaAtual = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
161
|
+
const baseSenderName = normalizeMetadataText(senderName, 'OmniZap System');
|
|
162
|
+
const resolvedUserId = String(getJidUser(userId) || userId || '').trim();
|
|
163
|
+
|
|
164
|
+
function doReplaces(str) {
|
|
165
|
+
return String(str ?? '')
|
|
166
|
+
.replace(/#nome/gi, baseSenderName)
|
|
167
|
+
.replace(/#data/gi, dataAtual)
|
|
168
|
+
.replace(/#hora/gi, horaAtual)
|
|
169
|
+
.replace(/#id/gi, resolvedUserId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const finalPackName = normalizeMetadataText(doReplaces(packName), 'OmniZap System');
|
|
173
|
+
const finalPackAuthor = normalizeMetadataText(doReplaces(packAuthor), baseSenderName);
|
|
174
|
+
|
|
175
|
+
logger.info(`addStickerMetadata Adicionando metadados ao sticker. Nome: "${finalPackName}", Autor: "${finalPackAuthor}"`);
|
|
176
|
+
|
|
177
|
+
let exifPath = null;
|
|
178
|
+
let outputPath = null;
|
|
179
|
+
let shouldKeepOutput = false;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await fs.mkdir(TEMP_DIR, { recursive: true });
|
|
183
|
+
await ensureWebpmuxAvailable();
|
|
184
|
+
|
|
185
|
+
const stickerDir = path.dirname(stickerPath) || TEMP_DIR;
|
|
186
|
+
await fs.mkdir(stickerDir, { recursive: true });
|
|
187
|
+
|
|
188
|
+
const exifData = {
|
|
189
|
+
'sticker-pack-id': `com.omnizap.${randomUUID()}`,
|
|
190
|
+
'sticker-pack-name': finalPackName,
|
|
191
|
+
'sticker-pack-publisher': finalPackAuthor,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const fileTag = randomUUID();
|
|
195
|
+
exifPath = path.join(TEMP_DIR, `exif_${fileTag}.exif`);
|
|
196
|
+
const exifAttr = Buffer.from([0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00]);
|
|
197
|
+
const jsonBuffer = Buffer.from(JSON.stringify(exifData), 'utf8');
|
|
198
|
+
const exifBuffer = Buffer.concat([exifAttr, jsonBuffer]);
|
|
199
|
+
exifBuffer.writeUIntLE(jsonBuffer.length, 14, 4);
|
|
200
|
+
|
|
201
|
+
await fs.writeFile(exifPath, exifBuffer);
|
|
202
|
+
|
|
203
|
+
outputPath = path.join(stickerDir, `final_${fileTag}.webp`);
|
|
204
|
+
await runProcess('webpmux', ['-set', 'exif', exifPath, stickerPath, '-o', outputPath], {
|
|
205
|
+
timeoutMs: WEBPMUX_EXEC_TIMEOUT_MS,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const outputStats = await fs.stat(outputPath);
|
|
209
|
+
if (!outputStats.isFile() || outputStats.size <= 0) {
|
|
210
|
+
throw new Error('Sticker final gerado inválido ao aplicar metadados.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
shouldKeepOutput = true;
|
|
214
|
+
logger.info(`addStickerMetadata Metadados adicionados com sucesso. Sticker final: ${outputPath}`);
|
|
215
|
+
return outputPath;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error.code === 'ENOENT') {
|
|
218
|
+
logger.error('addStickerMetadata webpmux não encontrado no PATH.');
|
|
219
|
+
}
|
|
220
|
+
logger.error(`addStickerMetadata Erro ao adicionar metadados: ${error.message}`, {
|
|
221
|
+
label: 'addStickerMetadata',
|
|
222
|
+
error: error.stack,
|
|
223
|
+
});
|
|
224
|
+
return stickerPath;
|
|
225
|
+
} finally {
|
|
226
|
+
const filesToClean = [exifPath];
|
|
227
|
+
if (!shouldKeepOutput && outputPath) {
|
|
228
|
+
filesToClean.push(outputPath);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const filePath of filesToClean.filter(Boolean)) {
|
|
232
|
+
await fs.unlink(filePath).catch((cleanupError) => {
|
|
233
|
+
if (cleanupError?.code !== 'ENOENT') {
|
|
234
|
+
logger.warn(`addStickerMetadata Falha ao limpar arquivo temporário ${filePath}: ${cleanupError.message}`);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
5
|
+
|
|
6
|
+
const TEMP_DIR = path.join(process.cwd(), 'temp', 'stickers');
|
|
7
|
+
const MB = 1024 * 1024;
|
|
8
|
+
const DEFAULT_MAX_OUTPUT_SIZE_BYTES_BY_TYPE = Object.freeze({
|
|
9
|
+
image: 1 * MB,
|
|
10
|
+
video: 2 * MB,
|
|
11
|
+
sticker: 2 * MB,
|
|
12
|
+
});
|
|
13
|
+
const DEFAULT_VIDEO_MAX_DURATION_SECONDS = 8;
|
|
14
|
+
const DEFAULT_VIDEO_FPS = 10;
|
|
15
|
+
const DEFAULT_VIDEO_QUALITY = 55;
|
|
16
|
+
const DEFAULT_VIDEO_COMPRESSION_LEVEL = 6;
|
|
17
|
+
const DEFAULT_TIMEOUT_MS_BY_TYPE = Object.freeze({
|
|
18
|
+
image: 15000,
|
|
19
|
+
sticker: 8000,
|
|
20
|
+
video: 30000,
|
|
21
|
+
});
|
|
22
|
+
const LOG_BUFFER_LIMIT = 16 * 1024;
|
|
23
|
+
const FFMPEG_WARNING_PATTERN = /\b(warn(?:ing)?|error|failed|invalid|deprecated)\b/i;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Mapa de timeout por tipo de mídia.
|
|
27
|
+
* @typedef {Object} TimeoutByTypeMap
|
|
28
|
+
* @property {number} [image]
|
|
29
|
+
* @property {number} [video]
|
|
30
|
+
* @property {number} [sticker]
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Mapa de tamanho máximo (em bytes) por tipo de mídia.
|
|
35
|
+
* @typedef {Object} MaxOutputSizeByTypeMap
|
|
36
|
+
* @property {number} [image]
|
|
37
|
+
* @property {number} [video]
|
|
38
|
+
* @property {number} [sticker]
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Opções de conversão para geração de sticker em WEBP.
|
|
43
|
+
* @typedef {Object} ConvertToWebpOptions
|
|
44
|
+
* @property {boolean} [stretch=true] - Se `true`, força `512x512` sem preservar proporção.
|
|
45
|
+
* @property {number} [videoMaxDurationSeconds=8] - Duração máxima aplicada ao vídeo (`-t`).
|
|
46
|
+
* @property {number} [videoFps=10] - FPS do vídeo durante a conversão.
|
|
47
|
+
* @property {number} [videoQuality=55] - Qualidade do vídeo em modo lossy (`-q:v`).
|
|
48
|
+
* @property {number} [videoCompressionLevel=6] - Compressão do vídeo (`-compression_level`).
|
|
49
|
+
* @property {number} [maxOutputSizeBytes] - Limite global de tamanho de saída em bytes.
|
|
50
|
+
* @property {MaxOutputSizeByTypeMap} [maxOutputSizeBytesByType] - Limite de saída por tipo.
|
|
51
|
+
* @property {TimeoutByTypeMap} [timeoutMsByType] - Timeout por tipo de mídia em milissegundos.
|
|
52
|
+
* @property {number} [timeoutMs] - Timeout explícito para a execução do `ffmpeg`.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resultado da execução de processo externo.
|
|
57
|
+
* @typedef {Object} ProcessExecutionResult
|
|
58
|
+
* @property {string} stdout
|
|
59
|
+
* @property {string} stderr
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Limita e normaliza um valor numérico em um intervalo.
|
|
64
|
+
*
|
|
65
|
+
* @param {number|string} value - Valor recebido.
|
|
66
|
+
* @param {number} min - Limite mínimo aceito.
|
|
67
|
+
* @param {number} max - Limite máximo aceito.
|
|
68
|
+
* @param {number} fallback - Valor padrão quando `value` é inválido.
|
|
69
|
+
* @returns {number} Valor normalizado.
|
|
70
|
+
*/
|
|
71
|
+
function clampNumber(value, min, max, fallback) {
|
|
72
|
+
const numericValue = Number(value);
|
|
73
|
+
if (!Number.isFinite(numericValue)) return fallback;
|
|
74
|
+
return Math.min(max, Math.max(min, numericValue));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve o timeout final da execução com prioridade para override explícito.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} mediaType - Tipo da mídia (`image`, `video`, `sticker`).
|
|
81
|
+
* @param {TimeoutByTypeMap} timeoutMsByType - Mapa de timeout por tipo.
|
|
82
|
+
* @param {number} [explicitTimeoutMs] - Timeout explícito para esta execução.
|
|
83
|
+
* @returns {number} Timeout final em milissegundos.
|
|
84
|
+
*/
|
|
85
|
+
function resolveTimeoutMs(mediaType, timeoutMsByType, explicitTimeoutMs) {
|
|
86
|
+
const directTimeout = Number(explicitTimeoutMs);
|
|
87
|
+
if (Number.isFinite(directTimeout) && directTimeout > 0) {
|
|
88
|
+
return Math.trunc(directTimeout);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const typedTimeout = Number(timeoutMsByType?.[mediaType]);
|
|
92
|
+
if (Number.isFinite(typedTimeout) && typedTimeout > 0) {
|
|
93
|
+
return Math.trunc(typedTimeout);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return DEFAULT_TIMEOUT_MS_BY_TYPE[mediaType] || 20000;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve o limite máximo de saída considerando override global e por tipo.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} mediaType - Tipo da mídia.
|
|
103
|
+
* @param {number} [explicitMaxOutputSizeBytes] - Limite global em bytes.
|
|
104
|
+
* @param {MaxOutputSizeByTypeMap} maxOutputSizeBytesByType - Limite específico por tipo.
|
|
105
|
+
* @returns {number} Limite final em bytes.
|
|
106
|
+
*/
|
|
107
|
+
function resolveMaxOutputLimit(mediaType, explicitMaxOutputSizeBytes, maxOutputSizeBytesByType) {
|
|
108
|
+
const directLimit = Number(explicitMaxOutputSizeBytes);
|
|
109
|
+
if (Number.isFinite(directLimit) && directLimit > 0) {
|
|
110
|
+
return Math.trunc(clampNumber(directLimit, 1, 20 * MB, DEFAULT_MAX_OUTPUT_SIZE_BYTES_BY_TYPE.image));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const typedLimit = Number(maxOutputSizeBytesByType?.[mediaType]);
|
|
114
|
+
if (Number.isFinite(typedLimit) && typedLimit > 0) {
|
|
115
|
+
return Math.trunc(clampNumber(typedLimit, 1, 20 * MB, DEFAULT_MAX_OUTPUT_SIZE_BYTES_BY_TYPE.image));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const fallbackLimit = DEFAULT_MAX_OUTPUT_SIZE_BYTES_BY_TYPE[mediaType] || DEFAULT_MAX_OUTPUT_SIZE_BYTES_BY_TYPE.image;
|
|
119
|
+
return Math.trunc(clampNumber(fallbackLimit, 1, 20 * MB, DEFAULT_MAX_OUTPUT_SIZE_BYTES_BY_TYPE.image));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Mantém apenas os últimos bytes do log para evitar crescimento infinito em memória.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} current - Buffer atual do log.
|
|
126
|
+
* @param {string} chunk - Novo trecho recebido.
|
|
127
|
+
* @returns {string} Buffer atualizado respeitando `LOG_BUFFER_LIMIT`.
|
|
128
|
+
*/
|
|
129
|
+
function appendBufferedLogs(current, chunk) {
|
|
130
|
+
const next = `${current}${chunk}`;
|
|
131
|
+
if (next.length <= LOG_BUFFER_LIMIT) return next;
|
|
132
|
+
return next.slice(-LOG_BUFFER_LIMIT);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Envia sinal de término para processo filho de forma segura.
|
|
137
|
+
*
|
|
138
|
+
* @param {import('node:child_process').ChildProcessWithoutNullStreams} child - Processo filho.
|
|
139
|
+
* @param {NodeJS.Signals} signal - Sinal a ser enviado.
|
|
140
|
+
* @returns {boolean} `true` se o sinal foi enviado, `false` em erro.
|
|
141
|
+
*/
|
|
142
|
+
function safeKill(child, signal) {
|
|
143
|
+
try {
|
|
144
|
+
return child.kill(signal);
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Retorna as últimas linhas não vazias de um texto de log.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} text - Conteúdo do log.
|
|
154
|
+
* @param {number} [maxLines=3] - Quantidade máxima de linhas retornadas.
|
|
155
|
+
* @returns {string} Trecho final do log.
|
|
156
|
+
*/
|
|
157
|
+
function getLogTail(text, maxLines = 3) {
|
|
158
|
+
if (!text) return '';
|
|
159
|
+
const lines = text
|
|
160
|
+
.split('\n')
|
|
161
|
+
.map((line) => line.trim())
|
|
162
|
+
.filter(Boolean);
|
|
163
|
+
if (!lines.length) return '';
|
|
164
|
+
return lines.slice(-maxLines).join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Define se o `stderr` do ffmpeg deve ser registrado em sucesso.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} stderrText - Conteúdo de stderr.
|
|
171
|
+
* @returns {boolean} `true` para logar; `false` para ignorar.
|
|
172
|
+
*/
|
|
173
|
+
function shouldLogFfmpegStderr(stderrText) {
|
|
174
|
+
const output = stderrText?.trim();
|
|
175
|
+
if (!output) return false;
|
|
176
|
+
if (process.env.DEBUG_FFMPEG === 'true') return true;
|
|
177
|
+
return FFMPEG_WARNING_PATTERN.test(output);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Executa um comando externo com timeout e coleta parcial de logs.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} command - Binário a executar.
|
|
184
|
+
* @param {string[]} args - Argumentos já separados (sem shell).
|
|
185
|
+
* @param {{ timeoutMs: number }} options - Configuração de execução.
|
|
186
|
+
* @returns {Promise<ProcessExecutionResult>} Saída capturada do processo.
|
|
187
|
+
*/
|
|
188
|
+
function runProcess(command, args, { timeoutMs }) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
191
|
+
let stdout = '';
|
|
192
|
+
let stderr = '';
|
|
193
|
+
let didTimeout = false;
|
|
194
|
+
|
|
195
|
+
const timeoutRef = setTimeout(() => {
|
|
196
|
+
didTimeout = true;
|
|
197
|
+
safeKill(child, 'SIGTERM');
|
|
198
|
+
setTimeout(() => safeKill(child, 'SIGKILL'), 1500);
|
|
199
|
+
}, timeoutMs);
|
|
200
|
+
|
|
201
|
+
child.stdout.on('data', (chunk) => {
|
|
202
|
+
stdout = appendBufferedLogs(stdout, chunk.toString());
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
child.stderr.on('data', (chunk) => {
|
|
206
|
+
stderr = appendBufferedLogs(stderr, chunk.toString());
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
child.on('error', (error) => {
|
|
210
|
+
clearTimeout(timeoutRef);
|
|
211
|
+
reject(error);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
child.on('close', (code, signal) => {
|
|
215
|
+
clearTimeout(timeoutRef);
|
|
216
|
+
|
|
217
|
+
if (didTimeout) {
|
|
218
|
+
const timeoutError = new Error(`Processo excedeu o timeout de ${timeoutMs}ms.`);
|
|
219
|
+
timeoutError.code = 'ETIMEDOUT';
|
|
220
|
+
timeoutError.signal = signal;
|
|
221
|
+
timeoutError.stderr = stderr;
|
|
222
|
+
reject(timeoutError);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (code !== 0) {
|
|
227
|
+
const processError = new Error(
|
|
228
|
+
`${command} finalizou com código ${code}${signal ? ` (sinal: ${signal})` : ''}.`,
|
|
229
|
+
);
|
|
230
|
+
processError.code = code;
|
|
231
|
+
processError.signal = signal;
|
|
232
|
+
processError.stderr = stderr;
|
|
233
|
+
processError.stdout = stdout;
|
|
234
|
+
reject(processError);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
resolve({ stdout, stderr });
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Valida arquivo gerado na conversão.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} filePath - Caminho do arquivo de saída.
|
|
247
|
+
* @param {number} maxOutputLimit - Limite máximo permitido em bytes.
|
|
248
|
+
* @returns {Promise<void>}
|
|
249
|
+
* @throws {Error} Quando o arquivo é inválido, vazio ou acima do limite.
|
|
250
|
+
*/
|
|
251
|
+
async function validateGeneratedFile(filePath, maxOutputLimit) {
|
|
252
|
+
const outputStats = await fs.stat(filePath);
|
|
253
|
+
if (!outputStats.isFile() || outputStats.size <= 0) {
|
|
254
|
+
throw new Error('Arquivo WEBP inválido ou vazio após a conversão.');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (outputStats.size > maxOutputLimit) {
|
|
258
|
+
const outputSizeMb = (outputStats.size / (1024 * 1024)).toFixed(2);
|
|
259
|
+
const maxSizeMb = (maxOutputLimit / (1024 * 1024)).toFixed(2);
|
|
260
|
+
throw new Error(`Arquivo gerado excedeu o limite (${outputSizeMb} MB > ${maxSizeMb} MB).`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Converte mídia para WEBP com parâmetros compatíveis com sticker do WhatsApp.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} inputPath - Caminho do arquivo de mídia de entrada.
|
|
268
|
+
* @param {'image'|'video'|'sticker'} mediaType - Tipo de mídia de entrada.
|
|
269
|
+
* @param {string} userId - Identificador do usuário para isolamento do diretório temporário.
|
|
270
|
+
* @param {string} uniqueId - Identificador único para o sticker.
|
|
271
|
+
* @param {ConvertToWebpOptions} [options={}] - Opções avançadas de conversão.
|
|
272
|
+
* @returns {Promise<string>} Caminho do arquivo webp gerado.
|
|
273
|
+
* @throws {Error} Se a conversão falhar.
|
|
274
|
+
*/
|
|
275
|
+
export async function convertToWebp(inputPath, mediaType, userId, uniqueId, options = {}) {
|
|
276
|
+
logger.info(`StickerCommand Convertendo mídia para webp. ID: ${uniqueId}, Tipo: ${mediaType}`);
|
|
277
|
+
const sanitizedUserId = String(userId || 'anon').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
278
|
+
const userStickerDir = path.join(TEMP_DIR, sanitizedUserId);
|
|
279
|
+
const outputPath = path.join(userStickerDir, `sticker_${uniqueId}.webp`);
|
|
280
|
+
const {
|
|
281
|
+
stretch = true,
|
|
282
|
+
videoMaxDurationSeconds = DEFAULT_VIDEO_MAX_DURATION_SECONDS,
|
|
283
|
+
videoFps = DEFAULT_VIDEO_FPS,
|
|
284
|
+
videoQuality = DEFAULT_VIDEO_QUALITY,
|
|
285
|
+
videoCompressionLevel = DEFAULT_VIDEO_COMPRESSION_LEVEL,
|
|
286
|
+
maxOutputSizeBytes,
|
|
287
|
+
maxOutputSizeBytesByType = DEFAULT_MAX_OUTPUT_SIZE_BYTES_BY_TYPE,
|
|
288
|
+
timeoutMsByType = DEFAULT_TIMEOUT_MS_BY_TYPE,
|
|
289
|
+
timeoutMs,
|
|
290
|
+
} = options;
|
|
291
|
+
const maxOutputLimit = resolveMaxOutputLimit(mediaType, maxOutputSizeBytes, maxOutputSizeBytesByType);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
await fs.mkdir(userStickerDir, { recursive: true });
|
|
295
|
+
const inputStats = await fs.stat(inputPath);
|
|
296
|
+
if (!inputStats.isFile() || inputStats.size <= 0) {
|
|
297
|
+
throw new Error('Arquivo de entrada inválido para conversão.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const allowedTypes = ['image', 'video', 'sticker'];
|
|
301
|
+
if (!allowedTypes.includes(mediaType)) {
|
|
302
|
+
logger.error(`Tipo de mídia não suportado para conversão: ${mediaType}`);
|
|
303
|
+
throw new Error(`Tipo de mídia não suportado: ${mediaType}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (mediaType === 'sticker') {
|
|
307
|
+
await fs.copyFile(inputPath, outputPath);
|
|
308
|
+
await validateGeneratedFile(outputPath, maxOutputLimit);
|
|
309
|
+
return outputPath;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const normalizedDuration = Math.trunc(
|
|
313
|
+
clampNumber(videoMaxDurationSeconds, 1, 30, DEFAULT_VIDEO_MAX_DURATION_SECONDS),
|
|
314
|
+
);
|
|
315
|
+
const normalizedFps = Math.trunc(clampNumber(videoFps, 1, 30, DEFAULT_VIDEO_FPS));
|
|
316
|
+
const normalizedQuality = Math.trunc(clampNumber(videoQuality, 0, 100, DEFAULT_VIDEO_QUALITY));
|
|
317
|
+
const normalizedCompression = Math.trunc(clampNumber(videoCompressionLevel, 0, 6, DEFAULT_VIDEO_COMPRESSION_LEVEL));
|
|
318
|
+
|
|
319
|
+
const stretchFilter = 'scale=512:512';
|
|
320
|
+
const scaleFilter = 'scale=512:512:force_original_aspect_ratio=decrease';
|
|
321
|
+
const padFilter = 'pad=512:512:(ow-iw)/2:(oh-ih)/2:color=0x00000000';
|
|
322
|
+
const imageFilter = stretch ? stretchFilter : `${scaleFilter},${padFilter}`;
|
|
323
|
+
|
|
324
|
+
const filterChain = mediaType === 'video' ? `fps=${normalizedFps},${imageFilter}` : imageFilter;
|
|
325
|
+
const ffmpegArgs = ['-y', '-i', inputPath];
|
|
326
|
+
|
|
327
|
+
if (mediaType === 'video') {
|
|
328
|
+
ffmpegArgs.push('-t', String(normalizedDuration));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
ffmpegArgs.push('-vcodec', 'libwebp', '-loop', '0', '-preset', 'default', '-an');
|
|
332
|
+
|
|
333
|
+
if (mediaType === 'video') {
|
|
334
|
+
ffmpegArgs.push(
|
|
335
|
+
'-vsync',
|
|
336
|
+
'0',
|
|
337
|
+
'-lossless',
|
|
338
|
+
'0',
|
|
339
|
+
'-q:v',
|
|
340
|
+
String(normalizedQuality),
|
|
341
|
+
'-compression_level',
|
|
342
|
+
String(normalizedCompression),
|
|
343
|
+
);
|
|
344
|
+
} else {
|
|
345
|
+
ffmpegArgs.push('-lossless', '1');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
ffmpegArgs.push('-vf', filterChain, outputPath);
|
|
349
|
+
|
|
350
|
+
const resolvedTimeoutMs = resolveTimeoutMs(mediaType, timeoutMsByType, timeoutMs);
|
|
351
|
+
let ffmpegResult;
|
|
352
|
+
try {
|
|
353
|
+
ffmpegResult = await runProcess('ffmpeg', ffmpegArgs, { timeoutMs: resolvedTimeoutMs });
|
|
354
|
+
} catch (ffmpegErr) {
|
|
355
|
+
if (ffmpegErr.code === 'ETIMEDOUT') {
|
|
356
|
+
logger.error('FFmpeg finalizado por timeout.');
|
|
357
|
+
throw new Error('Conversão cancelada: tempo limite excedido (timeout).');
|
|
358
|
+
}
|
|
359
|
+
if (ffmpegErr.code === 'ENOENT') {
|
|
360
|
+
logger.error('FFmpeg não encontrado no PATH do ambiente.');
|
|
361
|
+
throw new Error('FFmpeg não está instalado ou não está disponível no PATH do servidor.');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
logger.error(`Erro na execução do FFmpeg: ${ffmpegErr.message}`);
|
|
365
|
+
if (ffmpegErr.stderr) {
|
|
366
|
+
logger.error(`FFmpeg stderr: ${ffmpegErr.stderr}`);
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`Falha ao converter mídia para sticker (FFmpeg): ${ffmpegErr.message}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (ffmpegResult?.stderr && shouldLogFfmpegStderr(ffmpegResult.stderr)) {
|
|
372
|
+
const stderrTail = getLogTail(ffmpegResult.stderr, 3);
|
|
373
|
+
if (process.env.DEBUG_FFMPEG === 'true') {
|
|
374
|
+
logger.debug(`FFmpeg stderr: ${stderrTail}`);
|
|
375
|
+
} else {
|
|
376
|
+
logger.warn(`FFmpeg stderr (resumo): ${stderrTail}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await validateGeneratedFile(outputPath, maxOutputLimit);
|
|
381
|
+
|
|
382
|
+
logger.info(`StickerCommand Conversão bem-sucedida para: ${outputPath}`);
|
|
383
|
+
return outputPath;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
logger.error(`StickerCommand.convertToWebp Erro na conversão: ${error.message}`, {
|
|
386
|
+
error: error.stack,
|
|
387
|
+
});
|
|
388
|
+
throw new Error(`Erro na conversão para webp: ${error.message}`);
|
|
389
|
+
}
|
|
390
|
+
}
|