@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,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;
|