@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,657 @@
1
+ import { createCanvas } from 'canvas';
2
+ import { exec } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import logger from '../../utils/logger/loggerModule.js';
7
+
8
+ import { v4 as uuidv4 } from 'uuid';
9
+
10
+ import { convertToWebp } from './convertToWebp.js';
11
+ import { addStickerMetadata } from './addStickerMetadata.js';
12
+ import { getJidUser } from '../../config/baileysConfig.js';
13
+ import { sendAndStore } from '../../services/messagePersistenceService.js';
14
+ import { addStickerToAutoPack } from '../stickerPackModule/autoPackCollectorRuntime.js';
15
+
16
+ /**
17
+ * Constantes limitadoras
18
+ */
19
+ const MAX_CHARACTERS = 80;
20
+ const MAX_LINES = 4;
21
+
22
+ const TEMP_DIR = path.join(process.cwd(), 'temp', 'stickers');
23
+ const BLINK_FPS = 15;
24
+ const BLINK_DURATION_MS = 5000;
25
+ const BLINK_FREQ_HZ = 5;
26
+ const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
27
+ const AUTO_PACK_NOTICE_ENABLED = process.env.STICKER_PACK_AUTO_COLLECT_NOTIFY !== 'false';
28
+ const AUTO_PACK_MAX_ITEMS = Math.max(1, Number(process.env.STICKER_PACK_MAX_ITEMS) || 30);
29
+ const STICKER_WEB_PATH = normalizeBasePath(process.env.STICKER_WEB_PATH, '/stickers');
30
+ const STICKER_WEB_ORIGIN = resolveStickerWebOrigin();
31
+
32
+ function normalizeBasePath(value, fallback) {
33
+ const raw = String(value || '').trim() || fallback;
34
+ const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
35
+ const withoutTrailingSlash =
36
+ withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/')
37
+ ? withLeadingSlash.slice(0, -1)
38
+ : withLeadingSlash;
39
+ return withoutTrailingSlash || fallback;
40
+ }
41
+
42
+ function normalizeOrigin(value) {
43
+ const raw = String(value || '').trim();
44
+ if (!raw) return null;
45
+
46
+ try {
47
+ const parsed = new URL(raw);
48
+ if (!parsed.protocol || !parsed.host) return null;
49
+ return parsed.origin;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function resolveStickerWebOrigin() {
56
+ const candidates = [
57
+ process.env.STICKER_WEB_ORIGIN,
58
+ process.env.APP_BASE_URL,
59
+ process.env.PUBLIC_BASE_URL,
60
+ process.env.SITE_URL,
61
+ process.env.WEB_URL,
62
+ process.env.BASE_URL,
63
+ process.env.STICKER_DEFAULT_PACK_NAME,
64
+ ];
65
+
66
+ for (const candidate of candidates) {
67
+ const normalized = normalizeOrigin(candidate);
68
+ if (normalized) return normalized;
69
+ }
70
+
71
+ return 'https://omnizap.shop';
72
+ }
73
+
74
+ function buildPackWebUrl(packKey) {
75
+ const normalizedPackKey = String(packKey || '').trim();
76
+ if (!normalizedPackKey) return null;
77
+
78
+ return `${STICKER_WEB_ORIGIN}${STICKER_WEB_PATH}/${encodeURIComponent(normalizedPackKey)}`;
79
+ }
80
+
81
+ function isPackPubliclyVisible(pack) {
82
+ const visibility = String(pack?.visibility || '').trim().toLowerCase();
83
+ const status = String(pack?.status || 'published').trim().toLowerCase();
84
+ const packStatus = String(pack?.pack_status || 'ready').trim().toLowerCase();
85
+ return (visibility === 'public' || visibility === 'unlisted') && status === 'published' && packStatus === 'ready';
86
+ }
87
+
88
+ const execProm = promisify(exec);
89
+
90
+ const COLOR_ALIASES = {
91
+ branco: 'white',
92
+ white: 'white',
93
+ preto: 'black',
94
+ black: 'black',
95
+ vermelho: 'red',
96
+ red: 'red',
97
+ verde: 'green',
98
+ green: 'green',
99
+ azul: 'blue',
100
+ blue: 'blue',
101
+ amarelo: 'yellow',
102
+ yellow: 'yellow',
103
+ rosa: 'pink',
104
+ pink: 'pink',
105
+ roxo: 'purple',
106
+ purple: 'purple',
107
+ laranja: 'orange',
108
+ orange: 'orange',
109
+ };
110
+
111
+ /**
112
+ * Extrai uma cor no formato "-cor" no final do texto e retorna o texto limpo.
113
+ *
114
+ * @param {string} rawText
115
+ * @param {string} fallbackColor
116
+ * @returns {{ text: string, color: string }}
117
+ */
118
+ function parseColorFlag(rawText, fallbackColor) {
119
+ const trimmed = rawText.trim();
120
+ if (!trimmed) return { text: rawText, color: fallbackColor };
121
+
122
+ const match = trimmed.match(/(?:^|\s)-([a-zA-Z]+)\s*$/);
123
+ if (!match) return { text: rawText, color: fallbackColor };
124
+
125
+ const colorKey = match[1].toLowerCase();
126
+ const mapped = COLOR_ALIASES[colorKey];
127
+ if (!mapped) return { text: rawText, color: fallbackColor };
128
+
129
+ const cleanedText = trimmed.slice(0, match.index).trimEnd();
130
+ return { text: cleanedText, color: mapped };
131
+ }
132
+
133
+ function buildAutoPackNoticeText(result, commandPrefix = DEFAULT_COMMAND_PREFIX) {
134
+ if (!result || result.status === 'skipped') {
135
+ return null;
136
+ }
137
+
138
+ const pack = result.pack || {};
139
+ const packName = pack.name || 'Minhas Figurinhas';
140
+ const packIdentifier = pack.pack_key || pack.id || '<pack>';
141
+ const packWebUrl = isPackPubliclyVisible(pack) ? buildPackWebUrl(pack.pack_key) : null;
142
+ const profileUrl = `${STICKER_WEB_ORIGIN}${STICKER_WEB_PATH}/profile`;
143
+ const escapedPackName = packName.replace(/"/g, '\\"');
144
+ const packCommandTarget = escapedPackName ? `"${escapedPackName}"` : packIdentifier;
145
+ const itemCount = Array.isArray(pack.items) ? pack.items.length : Number(pack.sticker_count || 0);
146
+ const countLabel = itemCount > 0 ? ` (${itemCount}/${AUTO_PACK_MAX_ITEMS})` : '';
147
+
148
+ if (result.status === 'duplicate') {
149
+ const duplicateLines = [
150
+ `ℹ️ Essa figurinha já estava no pack automático *${packName}*.`,
151
+ `Use *${commandPrefix}pack info ${packIdentifier}* para ver o pack ou *${commandPrefix}pack send ${packIdentifier}* para enviar.`,
152
+ ];
153
+ if (packWebUrl) {
154
+ duplicateLines.push(`🌐 Link do pack no site: ${packWebUrl}`);
155
+ } else {
156
+ duplicateLines.push(`🔒 Pack privado/não publicado. Abra no painel: ${profileUrl}`);
157
+ }
158
+ return duplicateLines.join('\n');
159
+ }
160
+
161
+ const savedLines = [
162
+ `📦 Figurinha salva automaticamente no pack *${packName}*${countLabel}.\n\n`,
163
+ `Dica: use *${commandPrefix}pack list* para gerenciar seus packs.`,
164
+ `Para enviar agora: *${commandPrefix}pack send ${packCommandTarget}*.`,
165
+ ];
166
+ if (packWebUrl) {
167
+ savedLines.push(`🌐 Abrir no site: ${packWebUrl}`);
168
+ } else {
169
+ savedLines.push(`🔒 Pack privado/não publicado. Gerencie em: ${profileUrl}`);
170
+ }
171
+ return savedLines.join('\n');
172
+ }
173
+
174
+ async function notifyAutoPackCollection({ sock, remoteJid, messageInfo, expirationMessage, result, commandPrefix }) {
175
+ if (!AUTO_PACK_NOTICE_ENABLED) return;
176
+
177
+ const noticeText = buildAutoPackNoticeText(result, commandPrefix);
178
+ if (!noticeText) return;
179
+
180
+ await sendAndStore(
181
+ sock,
182
+ remoteJid,
183
+ { text: noticeText },
184
+ {
185
+ quoted: messageInfo,
186
+ ephemeralExpiration: expirationMessage,
187
+ },
188
+ );
189
+ }
190
+
191
+ /**
192
+ * Desenha texto centralizado no canvas com quebra de linha e ajuste de fonte.
193
+ *
194
+ * @param {CanvasRenderingContext2D} ctx
195
+ * @param {string} text
196
+ * @param {string} color
197
+ * @param {{ glow?: boolean }} [options]
198
+ */
199
+ function drawTextOnCanvas(ctx, text, color, { glow = false } = {}) {
200
+ const width = ctx.canvas.width;
201
+ const height = ctx.canvas.height;
202
+
203
+ const maxWidth = 460;
204
+
205
+ ctx.fillStyle = color ? color : 'black';
206
+ ctx.textAlign = 'center';
207
+ ctx.textBaseline = 'middle';
208
+
209
+ const charCount = text.replace(/\s+/g, '').length;
210
+ let fontSize = 56;
211
+ if (charCount <= 6) {
212
+ fontSize = 96;
213
+ } else if (charCount <= 10) {
214
+ fontSize = 80;
215
+ } else if (charCount <= 16) {
216
+ fontSize = 68;
217
+ }
218
+ ctx.font = `bold ${fontSize}px Arial`;
219
+
220
+ const wrapText = () => {
221
+ const words = text.split(' ');
222
+ const lines = [];
223
+ let currentLine = '';
224
+
225
+ const pushLine = () => {
226
+ if (currentLine) lines.push(currentLine.trim());
227
+ currentLine = '';
228
+ };
229
+
230
+ for (const word of words) {
231
+ const testLine = currentLine + word + ' ';
232
+ const testWidth = ctx.measureText(testLine).width;
233
+
234
+ if (testWidth <= maxWidth) {
235
+ currentLine = testLine;
236
+ continue;
237
+ }
238
+
239
+ if (currentLine) {
240
+ pushLine();
241
+ }
242
+
243
+ if (ctx.measureText(word).width <= maxWidth) {
244
+ currentLine = word + ' ';
245
+ continue;
246
+ }
247
+
248
+ let chunk = '';
249
+ for (const ch of word) {
250
+ const chunkTest = chunk + ch;
251
+ if (ctx.measureText(chunkTest).width > maxWidth && chunk) {
252
+ lines.push(chunk);
253
+ chunk = ch;
254
+ } else {
255
+ chunk = chunkTest;
256
+ }
257
+ }
258
+ if (chunk) lines.push(chunk);
259
+ }
260
+
261
+ pushLine();
262
+ return lines;
263
+ };
264
+
265
+ let lines = wrapText();
266
+ while (lines.length * fontSize > height - 40 || lines.some((line) => ctx.measureText(line).width > maxWidth)) {
267
+ fontSize -= 4;
268
+ ctx.font = `bold ${fontSize}px Arial`;
269
+ lines = wrapText();
270
+ }
271
+
272
+ const startY = height / 2 - (lines.length * fontSize) / 2;
273
+
274
+ lines.forEach((line, i) => {
275
+ if (glow) {
276
+ ctx.shadowColor = 'transparent';
277
+ ctx.shadowBlur = 0;
278
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.35)';
279
+ ctx.lineWidth = 3;
280
+ ctx.strokeText(line, width / 2, startY + i * fontSize);
281
+ }
282
+ ctx.fillText(line, width / 2, startY + i * fontSize);
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Gera uma imagem PNG (512x512) a partir de um texto.
288
+ *
289
+ * @param {string} text
290
+ * @param {string} outputDir
291
+ * @param {string} fileName (sem extensão)
292
+ * @returns {Promise<string>} Caminho do PNG gerado
293
+ */
294
+ /**
295
+ * Gera uma imagem PNG (512x512) a partir de um texto.
296
+ *
297
+ * @param {string} text
298
+ * @param {string} outputDir
299
+ * @param {string} fileName (sem extensão)
300
+ * @param {string} color
301
+ * @returns {Promise<string>} Caminho do PNG gerado
302
+ */
303
+ export async function generateTextImage(text, outputDir, fileName, color) {
304
+ try {
305
+ const width = 512;
306
+ const height = 512;
307
+
308
+ const canvas = createCanvas(width, height);
309
+ const ctx = canvas.getContext('2d');
310
+
311
+ ctx.clearRect(0, 0, width, height);
312
+ drawTextOnCanvas(ctx, text, color);
313
+
314
+ const buffer = canvas.toBuffer('image/png');
315
+ const outputPath = path.join(outputDir, `${fileName}.png`);
316
+
317
+ await fs.writeFile(outputPath, buffer);
318
+
319
+ logger.info(`Imagem de texto gerada em: ${outputPath}`);
320
+ return outputPath;
321
+ } catch (error) {
322
+ logger.error(`generateTextImage Erro ao gerar imagem: ${error.message}`);
323
+ throw error;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Gera um WebP animado com texto piscante.
329
+ *
330
+ * @param {string} text
331
+ * @param {string} outputDir
332
+ * @param {string} fileName (sem extensão)
333
+ * @param {string} [color='white']
334
+ * @returns {Promise<string>} Caminho do WebP gerado
335
+ */
336
+ async function generateBlinkingTextWebp(text, outputDir, fileName, color = 'white') {
337
+ const width = 512;
338
+ const height = 512;
339
+
340
+ const frameCount = Math.max(6, Math.round((BLINK_DURATION_MS / 1000) * BLINK_FPS));
341
+ const frameBaseName = `${fileName}_frame`;
342
+ const framePaths = [];
343
+
344
+ for (let i = 0; i < frameCount; i += 1) {
345
+ const frameIndex = String(i).padStart(3, '0');
346
+ const framePath = path.join(outputDir, `${frameBaseName}_${frameIndex}.png`);
347
+ framePaths.push(framePath);
348
+
349
+ const canvas = createCanvas(width, height);
350
+ const ctx = canvas.getContext('2d');
351
+ ctx.clearRect(0, 0, width, height);
352
+
353
+ const framesPerHalfCycle = Math.max(1, Math.round(BLINK_FPS / (BLINK_FREQ_HZ * 2)));
354
+ const alpha = Math.floor(i / framesPerHalfCycle) % 2 === 0 ? 1 : 0;
355
+ ctx.globalAlpha = alpha;
356
+ drawTextOnCanvas(ctx, text, color, { glow: true });
357
+ ctx.globalAlpha = 1;
358
+
359
+ const buffer = canvas.toBuffer('image/png');
360
+ await fs.writeFile(framePath, buffer);
361
+ }
362
+
363
+ const outputPath = path.join(outputDir, `${fileName}.webp`);
364
+ const frameDurationMs = Math.max(40, Math.round(1000 / BLINK_FPS));
365
+ const framesArg = framePaths.map((framePath) => `"${framePath}"`).join(' ');
366
+ const img2webpCommand = `img2webp -lossless -loop 0 -d ${frameDurationMs} ${framesArg} -o "${outputPath}"`;
367
+
368
+ try {
369
+ const img2webpResult = await execProm(img2webpCommand, { timeout: 20000 });
370
+ if (img2webpResult && img2webpResult.stderr) {
371
+ logger.debug(`img2webp stderr: ${img2webpResult.stderr}`);
372
+ }
373
+ await fs.access(outputPath);
374
+ } catch (error) {
375
+ logger.error(`generateBlinkingTextWebp Erro ao converter frames: ${error.message}`);
376
+ throw error;
377
+ } finally {
378
+ for (const framePath of framePaths) {
379
+ await fs.unlink(framePath).catch(() => {});
380
+ }
381
+ }
382
+
383
+ return outputPath;
384
+ }
385
+
386
+ /**
387
+ * Processa texto simples e envia sticker de texto estático.
388
+ *
389
+ * @param {object} params
390
+ * @param {object} params.sock
391
+ * @param {object} params.messageInfo
392
+ * @param {string} params.remoteJid
393
+ * @param {string} params.senderJid
394
+ * @param {string} params.senderName
395
+ * @param {string} params.text
396
+ * @param {number} params.expirationMessage
397
+ * @param {string} [params.extraText]
398
+ * @param {string} [params.color='black']
399
+ * @param {string} [params.commandPrefix='/']
400
+ * @returns {Promise<void>}
401
+ */
402
+ export async function processTextSticker({ sock, messageInfo, remoteJid, senderJid, senderName, text, expirationMessage, extraText = '', color = 'black', commandPrefix = DEFAULT_COMMAND_PREFIX }) {
403
+ const stickerText = text.trim();
404
+
405
+ if (!stickerText) {
406
+ await sendAndStore(
407
+ sock,
408
+ remoteJid,
409
+ {
410
+ text: '❌ Você precisa informar um texto para criar a figurinha.\n\n' + `Exemplo:\n*${commandPrefix}st bom dia seus lindos*`,
411
+ },
412
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
413
+ );
414
+
415
+ return;
416
+ }
417
+
418
+ if (stickerText.length > MAX_CHARACTERS) {
419
+ await sendAndStore(
420
+ sock,
421
+ remoteJid,
422
+ {
423
+ text: `❌ Texto muito longo: *${stickerText.length}* caracteres.\n` + `Limite atual: *${MAX_CHARACTERS}* caracteres.`,
424
+ },
425
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
426
+ );
427
+
428
+ return;
429
+ }
430
+
431
+ const stickerLines = stickerText.split(/\r?\n/);
432
+
433
+ if (stickerLines.length > MAX_LINES) {
434
+ await sendAndStore(
435
+ sock,
436
+ remoteJid,
437
+ {
438
+ text: `❌ Texto com muitas linhas: *${stickerLines.length}*.\n` + `Limite atual: *${MAX_LINES}* linhas.`,
439
+ },
440
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
441
+ );
442
+
443
+ return;
444
+ }
445
+
446
+ const uniqueId = uuidv4();
447
+ const userId = getJidUser(senderJid);
448
+ const sanitizedUserId = (userId || 'anon').replace(/[^a-zA-Z0-9.-]/g, '_');
449
+
450
+ let imagePath = null;
451
+ let webpPath = null;
452
+ let stickerPath = null;
453
+
454
+ try {
455
+ const userDir = path.join(TEMP_DIR, sanitizedUserId);
456
+ await fs.mkdir(userDir, { recursive: true });
457
+
458
+ imagePath = await generateTextImage(text, userDir, `text_${uniqueId}`, color);
459
+
460
+ webpPath = await convertToWebp(imagePath, 'image', sanitizedUserId, uniqueId);
461
+
462
+ const { packName, packAuthor } = (() => {
463
+ if (!extraText) {
464
+ return { packName: 'OmniZap Text', packAuthor: senderName };
465
+ }
466
+
467
+ const idx = extraText.indexOf('/');
468
+ return idx !== -1
469
+ ? {
470
+ packName: extraText.slice(0, idx).trim(),
471
+ packAuthor: extraText.slice(idx + 1).trim(),
472
+ }
473
+ : { packName: extraText.trim(), packAuthor: senderName };
474
+ })();
475
+
476
+ stickerPath = await addStickerMetadata(webpPath, packName, packAuthor, {
477
+ senderName,
478
+ userId,
479
+ });
480
+
481
+ const stickerBuffer = await fs.readFile(stickerPath);
482
+
483
+ await sendAndStore(sock, remoteJid, { sticker: stickerBuffer }, { ephemeralExpiration: expirationMessage });
484
+
485
+ setImmediate(() => {
486
+ addStickerToAutoPack({
487
+ ownerJid: senderJid,
488
+ senderName,
489
+ stickerBuffer,
490
+ })
491
+ .then((collectResult) =>
492
+ notifyAutoPackCollection({
493
+ sock,
494
+ remoteJid,
495
+ messageInfo,
496
+ expirationMessage,
497
+ result: collectResult,
498
+ commandPrefix,
499
+ }),
500
+ )
501
+ .catch((collectError) => {
502
+ logger.warn(`processTextSticker Falha ao coletar figurinha automática: ${collectError.message}`);
503
+ });
504
+ });
505
+ } catch (error) {
506
+ logger.error(`processTextSticker Erro: ${error.message}`, { error });
507
+ await sendAndStore(
508
+ sock,
509
+ remoteJid,
510
+ {
511
+ text: '*❌ Não foi possível criar a figurinha de texto.*\n\n' + `Tente novamente com outro texto ou use *${commandPrefix}st* com uma frase menor.`,
512
+ },
513
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
514
+ );
515
+ } finally {
516
+ const files = [imagePath, webpPath, stickerPath].filter(Boolean);
517
+ for (const file of files) {
518
+ await fs.unlink(file).catch(() => {});
519
+ }
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Processa texto e envia sticker animado com efeito de pisca-pisca.
525
+ *
526
+ * @param {object} params
527
+ * @param {object} params.sock
528
+ * @param {object} params.messageInfo
529
+ * @param {string} params.remoteJid
530
+ * @param {string} params.senderJid
531
+ * @param {string} params.senderName
532
+ * @param {string} params.text
533
+ * @param {number} params.expirationMessage
534
+ * @param {string} [params.extraText]
535
+ * @param {string} [params.color='white']
536
+ * @param {string} [params.commandPrefix='/']
537
+ * @returns {Promise<void>}
538
+ */
539
+ export async function processBlinkingTextSticker({ sock, messageInfo, remoteJid, senderJid, senderName, text, expirationMessage, extraText = '', color = 'white', commandPrefix = DEFAULT_COMMAND_PREFIX }) {
540
+ const parsed = parseColorFlag(text, color);
541
+ const stickerText = parsed.text.trim();
542
+ const resolvedColor = parsed.color;
543
+
544
+ if (!stickerText) {
545
+ await sendAndStore(
546
+ sock,
547
+ remoteJid,
548
+ {
549
+ text: '❌ Você precisa informar um texto para criar a figurinha piscante.\n\n' + `Exemplo:\n*${commandPrefix}stb bom dia seus lindos -verde*`,
550
+ },
551
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
552
+ );
553
+
554
+ return;
555
+ }
556
+
557
+ if (stickerText.length > MAX_CHARACTERS) {
558
+ await sendAndStore(
559
+ sock,
560
+ remoteJid,
561
+ {
562
+ text: `❌ Texto muito longo: *${stickerText.length}* caracteres.\n` + `Limite atual: *${MAX_CHARACTERS}* caracteres.`,
563
+ },
564
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
565
+ );
566
+
567
+ return;
568
+ }
569
+
570
+ const stickerLines = stickerText.split(/\r?\n/);
571
+
572
+ if (stickerLines.length > MAX_LINES) {
573
+ await sendAndStore(
574
+ sock,
575
+ remoteJid,
576
+ {
577
+ text: `❌ Texto com muitas linhas: *${stickerLines.length}*.\n` + `Limite atual: *${MAX_LINES}* linhas.`,
578
+ },
579
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
580
+ );
581
+
582
+ return;
583
+ }
584
+
585
+ const uniqueId = uuidv4();
586
+ const userId = getJidUser(senderJid);
587
+ const sanitizedUserId = (userId || 'anon').replace(/[^a-zA-Z0-9.-]/g, '_');
588
+
589
+ let webpPath = null;
590
+ let stickerPath = null;
591
+
592
+ try {
593
+ const userDir = path.join(TEMP_DIR, sanitizedUserId);
594
+ await fs.mkdir(userDir, { recursive: true });
595
+
596
+ webpPath = await generateBlinkingTextWebp(stickerText, userDir, `text_blink_${uniqueId}`, resolvedColor);
597
+
598
+ const { packName, packAuthor } = (() => {
599
+ if (!extraText) {
600
+ return { packName: 'OmniZap Blink', packAuthor: senderName };
601
+ }
602
+
603
+ const idx = extraText.indexOf('/');
604
+ return idx !== -1
605
+ ? {
606
+ packName: extraText.slice(0, idx).trim(),
607
+ packAuthor: extraText.slice(idx + 1).trim(),
608
+ }
609
+ : { packName: extraText.trim(), packAuthor: senderName };
610
+ })();
611
+
612
+ stickerPath = await addStickerMetadata(webpPath, packName, packAuthor, {
613
+ senderName,
614
+ userId,
615
+ });
616
+
617
+ const stickerBuffer = await fs.readFile(stickerPath);
618
+
619
+ await sendAndStore(sock, remoteJid, { sticker: stickerBuffer }, { ephemeralExpiration: expirationMessage });
620
+
621
+ setImmediate(() => {
622
+ addStickerToAutoPack({
623
+ ownerJid: senderJid,
624
+ senderName,
625
+ stickerBuffer,
626
+ })
627
+ .then((collectResult) =>
628
+ notifyAutoPackCollection({
629
+ sock,
630
+ remoteJid,
631
+ messageInfo,
632
+ expirationMessage,
633
+ result: collectResult,
634
+ commandPrefix,
635
+ }),
636
+ )
637
+ .catch((collectError) => {
638
+ logger.warn(`processBlinkingTextSticker Falha ao coletar figurinha automática: ${collectError.message}`);
639
+ });
640
+ });
641
+ } catch (error) {
642
+ logger.error(`processBlinkingTextSticker Erro: ${error.message}`, { error });
643
+ await sendAndStore(
644
+ sock,
645
+ remoteJid,
646
+ {
647
+ text: '*❌ Não foi possível criar a figurinha piscante.*\n\n' + `Tente novamente com outro texto ou use *${commandPrefix}stb* com uma frase menor.`,
648
+ },
649
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
650
+ );
651
+ } finally {
652
+ const files = [webpPath, stickerPath].filter(Boolean);
653
+ for (const file of files) {
654
+ await fs.unlink(file).catch(() => {});
655
+ }
656
+ }
657
+ }
@@ -0,0 +1,20 @@
1
+ import logger from '../../utils/logger/loggerModule.js';
2
+ import { createAutoPackCollector } from './autoPackCollectorService.js';
3
+ import stickerPackService from './stickerPackServiceRuntime.js';
4
+ import { saveStickerAssetFromBuffer } from './stickerStorageService.js';
5
+
6
+ /**
7
+ * Instância singleton do coletor automático de figurinhas.
8
+ */
9
+ const autoPackCollector = createAutoPackCollector({
10
+ logger,
11
+ stickerPackService,
12
+ saveStickerAssetFromBuffer,
13
+ });
14
+
15
+ /**
16
+ * Atalho para adicionar figurinhas ao pack automático do usuário.
17
+ */
18
+ export const addStickerToAutoPack = autoPackCollector.addStickerToAutoPack;
19
+
20
+ export default autoPackCollector;