@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,1095 @@
|
|
|
1
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
2
|
+
import { sendAndStore } from '../../services/messagePersistenceService.js';
|
|
3
|
+
import { isUserJid } from '../../config/baileysConfig.js';
|
|
4
|
+
import stickerPackService from './stickerPackServiceRuntime.js';
|
|
5
|
+
import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
|
|
6
|
+
import {
|
|
7
|
+
captureIncomingStickerAsset,
|
|
8
|
+
resolveStickerAssetForCommand,
|
|
9
|
+
} from './stickerStorageService.js';
|
|
10
|
+
import { buildStickerPackMessage, sendStickerPackWithFallback } from './stickerPackMessageService.js';
|
|
11
|
+
import { sanitizeText } from './stickerPackUtils.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handlers de comando textual para gerenciamento de packs de figurinha.
|
|
15
|
+
*/
|
|
16
|
+
const RATE_WINDOW_MS = Math.max(10_000, Number(process.env.STICKER_PACK_RATE_WINDOW_MS) || 60_000);
|
|
17
|
+
const RATE_MAX_ACTIONS = Math.max(1, Number(process.env.STICKER_PACK_RATE_MAX_ACTIONS) || 20);
|
|
18
|
+
const MAX_PACK_ITEMS = Math.max(1, Number(process.env.STICKER_PACK_MAX_ITEMS) || 30);
|
|
19
|
+
const MAX_PACK_NAME_LENGTH = 120;
|
|
20
|
+
|
|
21
|
+
const rateMap = new Map();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Separa texto em subcomando e argumentos restantes.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} text Texto bruto após `pack`.
|
|
27
|
+
* @returns {{ command: string, rest: string }} Partes do comando.
|
|
28
|
+
*/
|
|
29
|
+
const extractCommandParts = (text) => {
|
|
30
|
+
const raw = String(text || '').trim();
|
|
31
|
+
if (!raw) return { command: '', rest: '' };
|
|
32
|
+
|
|
33
|
+
const firstSpace = raw.indexOf(' ');
|
|
34
|
+
if (firstSpace === -1) {
|
|
35
|
+
return { command: raw.toLowerCase(), rest: '' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
command: raw.slice(0, firstSpace).toLowerCase(),
|
|
40
|
+
rest: raw.slice(firstSpace + 1).trim(),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remove aspas simples/duplas de borda e trim.
|
|
46
|
+
*
|
|
47
|
+
* @param {unknown} value Valor textual.
|
|
48
|
+
* @returns {string} Texto sem aspas externas.
|
|
49
|
+
*/
|
|
50
|
+
const unquote = (value) => {
|
|
51
|
+
const raw = String(value || '').trim();
|
|
52
|
+
if (!raw) return '';
|
|
53
|
+
|
|
54
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
55
|
+
return raw.slice(1, -1).trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return raw;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Lê o primeiro token (com suporte a aspas) e retorna o restante.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} input Texto de entrada.
|
|
65
|
+
* @returns {{ token: string|null, rest: string }} Token e restante.
|
|
66
|
+
*/
|
|
67
|
+
const readToken = (input) => {
|
|
68
|
+
const raw = String(input || '').trim();
|
|
69
|
+
if (!raw) return { token: null, rest: '' };
|
|
70
|
+
|
|
71
|
+
const match = raw.match(/^("([^"\\]|\\.)*"|'([^'\\]|\\.)*'|\S+)([\s\S]*)$/);
|
|
72
|
+
if (!match) return { token: null, rest: '' };
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
token: unquote(match[1]),
|
|
76
|
+
rest: (match[4] || '').trim(),
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Divide argumentos por `|`, removendo segmentos vazios.
|
|
82
|
+
*
|
|
83
|
+
* @param {unknown} value Texto com opções pipe.
|
|
84
|
+
* @returns {string[]} Segmentos limpos.
|
|
85
|
+
*/
|
|
86
|
+
const splitPipeSegments = (value) =>
|
|
87
|
+
String(value || '')
|
|
88
|
+
.split('|')
|
|
89
|
+
.map((segment) => segment.trim())
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Converte segmentos `chave=valor` em objeto de opções.
|
|
94
|
+
*
|
|
95
|
+
* @param {string[]} segments Segmentos já divididos por pipe.
|
|
96
|
+
* @returns {Record<string, string>} Mapa de opções.
|
|
97
|
+
*/
|
|
98
|
+
const parsePipeOptions = (segments) => {
|
|
99
|
+
const options = {};
|
|
100
|
+
|
|
101
|
+
for (const segment of segments) {
|
|
102
|
+
const eqIndex = segment.indexOf('=');
|
|
103
|
+
if (eqIndex <= 0) continue;
|
|
104
|
+
|
|
105
|
+
const key = segment.slice(0, eqIndex).trim().toLowerCase();
|
|
106
|
+
const value = unquote(segment.slice(eqIndex + 1));
|
|
107
|
+
|
|
108
|
+
if (!key) continue;
|
|
109
|
+
options[key] = value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return options;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Envia resposta textual no chat preservando contexto/ephemeral.
|
|
117
|
+
*
|
|
118
|
+
* @param {{ sock: object, remoteJid: string, messageInfo: object, expirationMessage: number|undefined, text: string }} params Contexto de envio.
|
|
119
|
+
* @returns {Promise<object>} Resultado de envio.
|
|
120
|
+
*/
|
|
121
|
+
const sendReply = async ({ sock, remoteJid, messageInfo, expirationMessage, text }) =>
|
|
122
|
+
sendAndStore(
|
|
123
|
+
sock,
|
|
124
|
+
remoteJid,
|
|
125
|
+
{ text },
|
|
126
|
+
{
|
|
127
|
+
quoted: messageInfo,
|
|
128
|
+
ephemeralExpiration: expirationMessage,
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const PACK_VISUAL_DIVIDER = '━━━━━━━━━━━━━━━━━━━';
|
|
133
|
+
const PACK_VISUAL_HEADER = '📦 *PACKS DE FIGURINHAS — CENTRAL DE GERENCIAMENTO*';
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Normaliza blocos de texto para linhas não vazias.
|
|
137
|
+
*
|
|
138
|
+
* @param {unknown} value Texto, lista de textos ou aninhamento.
|
|
139
|
+
* @returns {string[]} Linhas prontas para renderização.
|
|
140
|
+
*/
|
|
141
|
+
const normalizeMessageLines = (value) => {
|
|
142
|
+
if (value === null || value === undefined) return [];
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
return value.flatMap((item) => normalizeMessageLines(item));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const line = String(value).trim();
|
|
148
|
+
return line ? [line] : [];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Formata rótulo visual de visibilidade do pack.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} visibility Visibilidade interna.
|
|
155
|
+
* @returns {string} Rótulo amigável com ícone.
|
|
156
|
+
*/
|
|
157
|
+
const formatVisibilityLabel = (visibility) => {
|
|
158
|
+
const normalized = String(visibility || '').toLowerCase();
|
|
159
|
+
if (normalized === 'public') return '🌍 Público';
|
|
160
|
+
if (normalized === 'unlisted') return '🔗 Não listado';
|
|
161
|
+
return '🔒 Privado';
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Monta mensagem visual padronizada para comandos de pack.
|
|
166
|
+
*
|
|
167
|
+
* @param {{ intro?: unknown[], sections?: Array<{title?: string, lines?: unknown[]}>, footer?: unknown[] }} params Blocos da mensagem.
|
|
168
|
+
* @returns {string} Texto final formatado.
|
|
169
|
+
*/
|
|
170
|
+
const buildPackVisualMessage = ({ intro = [], sections = [], footer = [] }) => {
|
|
171
|
+
const lines = [PACK_VISUAL_HEADER];
|
|
172
|
+
const introLines = normalizeMessageLines(intro);
|
|
173
|
+
if (introLines.length) {
|
|
174
|
+
lines.push('', ...introLines);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const section of sections) {
|
|
178
|
+
if (!section) continue;
|
|
179
|
+
const title = section.title ? String(section.title).trim() : '';
|
|
180
|
+
const sectionLines = normalizeMessageLines(section.lines);
|
|
181
|
+
if (!title && !sectionLines.length) continue;
|
|
182
|
+
|
|
183
|
+
lines.push('', PACK_VISUAL_DIVIDER);
|
|
184
|
+
if (title) lines.push(title);
|
|
185
|
+
if (sectionLines.length) {
|
|
186
|
+
lines.push('', ...sectionLines);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const footerLines = normalizeMessageLines(footer);
|
|
191
|
+
if (footerLines.length) {
|
|
192
|
+
lines.push('', PACK_VISUAL_DIVIDER, ...footerLines);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Monta template visual para ações de sucesso/instrução.
|
|
200
|
+
*
|
|
201
|
+
* @param {{ title: string, explanation?: unknown[], details?: unknown[], nextSteps?: unknown[], footer?: unknown[] }} params Dados da mensagem.
|
|
202
|
+
* @returns {string} Texto final.
|
|
203
|
+
*/
|
|
204
|
+
const buildActionMessage = ({ title, explanation = [], details = [], nextSteps = [], footer = [] }) =>
|
|
205
|
+
buildPackVisualMessage({
|
|
206
|
+
intro: [title, ...normalizeMessageLines(explanation)],
|
|
207
|
+
sections: [
|
|
208
|
+
normalizeMessageLines(details).length ? { title: '📌 *DETALHES*', lines: details } : null,
|
|
209
|
+
normalizeMessageLines(nextSteps).length ? { title: '➡️ *PRÓXIMAS AÇÕES*', lines: nextSteps } : null,
|
|
210
|
+
],
|
|
211
|
+
footer,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Renderiza listagem de packs em formato amigável para chat.
|
|
216
|
+
*
|
|
217
|
+
* @param {object[]} packs Lista de packs do usuário.
|
|
218
|
+
* @param {string} prefix Prefixo de comando.
|
|
219
|
+
* @returns {string} Mensagem formatada.
|
|
220
|
+
*/
|
|
221
|
+
const formatPackList = (packs, prefix) => {
|
|
222
|
+
if (!packs.length) {
|
|
223
|
+
return buildPackVisualMessage({
|
|
224
|
+
intro: [
|
|
225
|
+
'📭 *Nenhum pack extra encontrado.*',
|
|
226
|
+
'As figurinhas que você cria continuam sendo salvas automaticamente no seu *Pack Principal*.',
|
|
227
|
+
],
|
|
228
|
+
sections: [
|
|
229
|
+
{
|
|
230
|
+
title: '🆕 *COMECE EM 3 PASSOS*',
|
|
231
|
+
lines: [
|
|
232
|
+
`1) Crie um pack: \`${prefix}pack create meupack\``,
|
|
233
|
+
`2) Responda uma figurinha e adicione: \`${prefix}pack add <pack>\``,
|
|
234
|
+
`3) Veja o resumo: \`${prefix}pack info <pack>\``,
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
footer: ['💡 Dica: crie packs por tema (memes, animes, reactions) para achar tudo mais rápido.'],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const lines = packs.map((pack, index) => {
|
|
243
|
+
const count = Number(pack.sticker_count || 0);
|
|
244
|
+
return [
|
|
245
|
+
`${index + 1}. *${pack.name}*`,
|
|
246
|
+
` 🆔 ID: \`${pack.pack_key}\``,
|
|
247
|
+
` 🧩 Itens: ${count}/${MAX_PACK_ITEMS}`,
|
|
248
|
+
` 👁️ Visibilidade: ${formatVisibilityLabel(pack.visibility)}`,
|
|
249
|
+
].join('\n');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return buildPackVisualMessage({
|
|
253
|
+
intro: [
|
|
254
|
+
`📋 *Packs encontrados: ${packs.length}*`,
|
|
255
|
+
'Você pode usar o *nome* ou o *ID* do pack para ver detalhes, editar ou enviar.',
|
|
256
|
+
],
|
|
257
|
+
sections: [
|
|
258
|
+
{ title: '📦 *SEUS PACKS*', lines },
|
|
259
|
+
{
|
|
260
|
+
title: '🛠 *ATALHOS*',
|
|
261
|
+
lines: [
|
|
262
|
+
`ℹ️ Detalhes: \`${prefix}pack info <pack>\``,
|
|
263
|
+
`📤 Enviar: \`${prefix}pack send <pack>\``,
|
|
264
|
+
`🆕 Criar novo: \`${prefix}pack create meupack\``,
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
footer: ['✅ Tudo pronto — escolha um pack e continue gerenciando.'],
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Renderiza detalhes completos de um pack com preview de itens.
|
|
274
|
+
*
|
|
275
|
+
* @param {{ items: object[], cover_sticker_id?: string, name: string, pack_key: string, publisher: string, visibility: string, description?: string }} pack Pack completo.
|
|
276
|
+
* @param {string} prefix Prefixo de comando.
|
|
277
|
+
* @returns {string} Mensagem formatada.
|
|
278
|
+
*/
|
|
279
|
+
const formatPackInfo = (pack, prefix) => {
|
|
280
|
+
const coverIndex = pack.items.findIndex((item) => item.sticker_id === pack.cover_sticker_id);
|
|
281
|
+
const coverLabel = coverIndex >= 0 ? `figurinha #${coverIndex + 1}` : 'não definida';
|
|
282
|
+
const itemLines = pack.items.slice(0, 12).map((item, index) => {
|
|
283
|
+
const emojis = Array.isArray(item.emojis) && item.emojis.length ? ` ${item.emojis.join(' ')}` : '';
|
|
284
|
+
const coverTag = item.sticker_id === pack.cover_sticker_id ? ' 🖼️ *Capa*' : '';
|
|
285
|
+
return `${index + 1}. \`${item.sticker_id.slice(0, 10)}\`${emojis}${coverTag}`;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (pack.items.length > 12) {
|
|
289
|
+
itemLines.push(`… e mais ${pack.items.length - 12} figurinha(s).`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return buildPackVisualMessage({
|
|
293
|
+
intro: [
|
|
294
|
+
`ℹ️ *Informações do pack: "${pack.name}"*`,
|
|
295
|
+
'Aqui você vê identificação, visibilidade e uma prévia dos itens cadastrados.',
|
|
296
|
+
],
|
|
297
|
+
sections: [
|
|
298
|
+
{
|
|
299
|
+
title: '📌 *DADOS DO PACK*',
|
|
300
|
+
lines: [
|
|
301
|
+
`📛 Nome: *${pack.name}*`,
|
|
302
|
+
`🆔 ID: \`${pack.pack_key}\``,
|
|
303
|
+
`👤 Publisher: *${pack.publisher}*`,
|
|
304
|
+
`👁️ Visibilidade: ${formatVisibilityLabel(pack.visibility)}`,
|
|
305
|
+
`🧩 Itens: *${pack.items.length}/${MAX_PACK_ITEMS}*`,
|
|
306
|
+
`🖼️ Capa: *${coverLabel}*`,
|
|
307
|
+
`📝 Descrição: ${pack.description ? `"${pack.description}"` : 'não definida'}`,
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
title: '🖼️ *PRÉVIA (ATÉ 12 ITENS)*',
|
|
312
|
+
lines: itemLines.length ? itemLines : ['Nenhuma figurinha cadastrada neste pack ainda.'],
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
title: '⚙️ *AÇÕES DISPONÍVEIS*',
|
|
316
|
+
lines: [
|
|
317
|
+
`➕ Adicionar: \`${prefix}pack add ${pack.pack_key}\``,
|
|
318
|
+
`🖼 Definir capa: \`${prefix}pack setcover ${pack.pack_key}\``,
|
|
319
|
+
`🔀 Reordenar: \`${prefix}pack reorder ${pack.pack_key} 1 2 3 ...\``,
|
|
320
|
+
`📤 Enviar: \`${prefix}pack send ${pack.pack_key}\``,
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
footer: ['💡 Se precisar, use o guia completo com `pack` para ver exemplos e comandos extras.'],
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Retorna texto de ajuda principal dos comandos de pack.
|
|
330
|
+
*
|
|
331
|
+
* @param {string} prefix Prefixo de comando.
|
|
332
|
+
* @returns {string} Guia textual.
|
|
333
|
+
*/
|
|
334
|
+
const buildPackHelp = (prefix) =>
|
|
335
|
+
[
|
|
336
|
+
'📦 *PACKS DE FIGURINHAS — GUIA RÁPIDO*',
|
|
337
|
+
'',
|
|
338
|
+
'Toda figurinha que você criar é salva automaticamente no seu *Pack Principal*.',
|
|
339
|
+
'Além disso, você pode criar packs extras para organizar por tema e enviar mais rápido.',
|
|
340
|
+
'',
|
|
341
|
+
PACK_VISUAL_DIVIDER,
|
|
342
|
+
'🧭 *COMANDOS PRINCIPAIS*',
|
|
343
|
+
'',
|
|
344
|
+
'🆕 Criar um pack',
|
|
345
|
+
`\`${prefix}pack create "Meus memes 😂" | publisher="Seu Nome" | desc="Descrição"\``,
|
|
346
|
+
'_Nome livre: espaços e emojis são permitidos._',
|
|
347
|
+
'',
|
|
348
|
+
'📋 Listar packs',
|
|
349
|
+
`\`${prefix}pack list\``,
|
|
350
|
+
'',
|
|
351
|
+
'ℹ️ Ver detalhes do pack',
|
|
352
|
+
`\`${prefix}pack info <pack>\``,
|
|
353
|
+
'',
|
|
354
|
+
'➕ Adicionar figurinha',
|
|
355
|
+
`\`${prefix}pack add <pack>\``,
|
|
356
|
+
'_Dica: responda uma figurinha (ou use a última enviada)._',
|
|
357
|
+
'',
|
|
358
|
+
'🖼 Definir capa',
|
|
359
|
+
`\`${prefix}pack setcover <pack>\``,
|
|
360
|
+
'',
|
|
361
|
+
'📤 Enviar pack no chat',
|
|
362
|
+
`\`${prefix}pack send "<nome do pack>"\``,
|
|
363
|
+
`_Ou use o ID: \`${prefix}pack send <pack_id>\`_`,
|
|
364
|
+
'',
|
|
365
|
+
PACK_VISUAL_DIVIDER,
|
|
366
|
+
'🧰 *COMANDOS EXTRAS*',
|
|
367
|
+
'',
|
|
368
|
+
'`rename` • `setpub` • `setdesc` • `remove` • `reorder` • `clone` • `publish` • `delete`',
|
|
369
|
+
'',
|
|
370
|
+
PACK_VISUAL_DIVIDER,
|
|
371
|
+
'✅ *Pronto!* Se quiser, diga o que você quer fazer (criar, organizar, enviar) que eu te guio.',
|
|
372
|
+
].join('\n');
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Template visual de erro orientado a resolução.
|
|
376
|
+
*
|
|
377
|
+
* @param {{ title: string, explanation?: unknown[], steps?: unknown[], commandPrefix: string }} params Conteúdo do erro.
|
|
378
|
+
* @returns {string} Texto formatado.
|
|
379
|
+
*/
|
|
380
|
+
const buildErrorMessage = ({ title, explanation = [], steps = [], commandPrefix }) =>
|
|
381
|
+
buildPackVisualMessage({
|
|
382
|
+
intro: [title, ...normalizeMessageLines(explanation)],
|
|
383
|
+
sections: [
|
|
384
|
+
normalizeMessageLines(steps).length
|
|
385
|
+
? {
|
|
386
|
+
title: '🧭 *COMO RESOLVER*',
|
|
387
|
+
lines: steps,
|
|
388
|
+
}
|
|
389
|
+
: null,
|
|
390
|
+
],
|
|
391
|
+
footer: [`💡 Guia completo: \`${commandPrefix}pack\``],
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Converte erro interno em mensagem amigável para usuário.
|
|
396
|
+
*
|
|
397
|
+
* @param {unknown} error Erro recebido durante o comando.
|
|
398
|
+
* @param {string} commandPrefix Prefixo configurado.
|
|
399
|
+
* @returns {string} Mensagem final de erro.
|
|
400
|
+
*/
|
|
401
|
+
const formatErrorMessage = (error, commandPrefix) => {
|
|
402
|
+
if (!(error instanceof StickerPackError)) {
|
|
403
|
+
return buildErrorMessage({
|
|
404
|
+
title: '❌ *Não consegui concluir sua solicitação.*',
|
|
405
|
+
explanation: ['Ocorreu um erro inesperado ao processar o comando.'],
|
|
406
|
+
steps: ['Aguarde alguns segundos e tente novamente.'],
|
|
407
|
+
commandPrefix,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
switch (error.code) {
|
|
412
|
+
case STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND:
|
|
413
|
+
return buildErrorMessage({
|
|
414
|
+
title: '🔎 *Pack não encontrado.*',
|
|
415
|
+
explanation: ['Não localizei um pack com esse nome ou ID.'],
|
|
416
|
+
steps: [
|
|
417
|
+
`Veja a lista com \`${commandPrefix}pack list\`.`,
|
|
418
|
+
'Copie o ID exatamente como aparece.',
|
|
419
|
+
`Depois tente novamente (ex.: \`${commandPrefix}pack info <pack>\`).`,
|
|
420
|
+
],
|
|
421
|
+
commandPrefix,
|
|
422
|
+
});
|
|
423
|
+
case STICKER_PACK_ERROR_CODES.DUPLICATE_STICKER:
|
|
424
|
+
return buildErrorMessage({
|
|
425
|
+
title: '⚠️ *Essa figurinha já está no pack.*',
|
|
426
|
+
explanation: ['Para manter o pack organizado, não adiciono itens duplicados.'],
|
|
427
|
+
steps: [
|
|
428
|
+
`Veja os itens com \`${commandPrefix}pack info <pack>\`.`,
|
|
429
|
+
'Se quiser reorganizar, use `reorder`.',
|
|
430
|
+
],
|
|
431
|
+
commandPrefix,
|
|
432
|
+
});
|
|
433
|
+
case STICKER_PACK_ERROR_CODES.PACK_LIMIT_REACHED:
|
|
434
|
+
return buildErrorMessage({
|
|
435
|
+
title: '⚠️ *Limite de figurinhas atingido.*',
|
|
436
|
+
explanation: [error.message || 'Este pack já está no limite e não aceita novos itens no momento.'],
|
|
437
|
+
steps: [
|
|
438
|
+
`Crie outro pack: \`${commandPrefix}pack create novopack\`.`,
|
|
439
|
+
'Depois continue adicionando as próximas figurinhas no novo pack.',
|
|
440
|
+
],
|
|
441
|
+
commandPrefix,
|
|
442
|
+
});
|
|
443
|
+
case STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND:
|
|
444
|
+
return buildErrorMessage({
|
|
445
|
+
title: '🧩 *Não encontrei uma figurinha válida para usar.*',
|
|
446
|
+
explanation: ['Para esse comando, você precisa responder uma figurinha ou ter uma figurinha recente no contexto.'],
|
|
447
|
+
steps: [
|
|
448
|
+
'Responda diretamente a figurinha que você quer usar.',
|
|
449
|
+
'Ou envie uma figurinha e execute o comando novamente.',
|
|
450
|
+
],
|
|
451
|
+
commandPrefix,
|
|
452
|
+
});
|
|
453
|
+
case STICKER_PACK_ERROR_CODES.INVALID_INPUT:
|
|
454
|
+
return buildErrorMessage({
|
|
455
|
+
title: '⚠️ *Formato do comando inválido.*',
|
|
456
|
+
explanation: [error.message || 'Revise o formato do comando e tente novamente.'],
|
|
457
|
+
steps: [`Abra os exemplos: \`${commandPrefix}pack\`.`],
|
|
458
|
+
commandPrefix,
|
|
459
|
+
});
|
|
460
|
+
case STICKER_PACK_ERROR_CODES.STORAGE_ERROR:
|
|
461
|
+
return buildErrorMessage({
|
|
462
|
+
title: '💾 *Falha ao acessar os dados do pack.*',
|
|
463
|
+
explanation: [error.message || 'Os arquivos não ficaram disponíveis agora.'],
|
|
464
|
+
steps: ['Tente novamente em instantes. Se persistir, envie o comando usado para eu analisar.'],
|
|
465
|
+
commandPrefix,
|
|
466
|
+
});
|
|
467
|
+
default:
|
|
468
|
+
return buildErrorMessage({
|
|
469
|
+
title: '❌ *Erro ao gerenciar packs.*',
|
|
470
|
+
explanation: [error.message || 'Ocorreu um erro interno durante a operação.'],
|
|
471
|
+
steps: ['Tente novamente e, se continuar, compartilhe o comando usado para análise.'],
|
|
472
|
+
commandPrefix,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Aplica rate limit por usuário para comandos de pack.
|
|
479
|
+
*
|
|
480
|
+
* @param {string} ownerJid JID do dono do comando.
|
|
481
|
+
* @returns {{ limited: boolean, remainingMs: number }} Estado do limite.
|
|
482
|
+
*/
|
|
483
|
+
const checkRateLimit = (ownerJid) => {
|
|
484
|
+
const now = Date.now();
|
|
485
|
+
const entry = rateMap.get(ownerJid);
|
|
486
|
+
|
|
487
|
+
if (!entry || entry.resetAt <= now) {
|
|
488
|
+
rateMap.set(ownerJid, {
|
|
489
|
+
count: 1,
|
|
490
|
+
resetAt: now + RATE_WINDOW_MS,
|
|
491
|
+
});
|
|
492
|
+
return { limited: false, remainingMs: 0 };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (entry.count >= RATE_MAX_ACTIONS) {
|
|
496
|
+
return {
|
|
497
|
+
limited: true,
|
|
498
|
+
remainingMs: entry.resetAt - now,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
entry.count += 1;
|
|
503
|
+
rateMap.set(ownerJid, entry);
|
|
504
|
+
return { limited: false, remainingMs: 0 };
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Lê identificador inicial e restante textual do comando.
|
|
509
|
+
*
|
|
510
|
+
* @param {string} input Texto de entrada.
|
|
511
|
+
* @returns {{ identifier: string|null, value: string }} Partes parseadas.
|
|
512
|
+
*/
|
|
513
|
+
const parseIdentifierAndValue = (input) => {
|
|
514
|
+
const { token: identifier, rest } = readToken(input);
|
|
515
|
+
return {
|
|
516
|
+
identifier,
|
|
517
|
+
value: unquote(rest),
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const readSingleArgument = (input) => {
|
|
522
|
+
const value = unquote(input);
|
|
523
|
+
return value ? value : null;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Normaliza e valida nome de pack (permite espaços e emojis).
|
|
528
|
+
*
|
|
529
|
+
* @param {string} value Nome informado.
|
|
530
|
+
* @param {{ label?: string }} [options] Label para mensagens de erro.
|
|
531
|
+
* @returns {string} Nome normalizado.
|
|
532
|
+
* @throws {StickerPackError} Quando o nome estiver vazio.
|
|
533
|
+
*/
|
|
534
|
+
const normalizePackName = (value, { label = 'Nome do pack' } = {}) => {
|
|
535
|
+
const normalized = sanitizeText(unquote(value), MAX_PACK_NAME_LENGTH, { allowEmpty: false });
|
|
536
|
+
|
|
537
|
+
if (!normalized) {
|
|
538
|
+
throw new StickerPackError(STICKER_PACK_ERROR_CODES.INVALID_INPUT, `${label} é obrigatório.`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return normalized;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Converte entrada de reordenação em lista de sticker IDs.
|
|
546
|
+
*
|
|
547
|
+
* @param {{ ownerJid: string, identifier: string, rawOrder: string }} params Contexto de reordenação.
|
|
548
|
+
* @returns {Promise<string[]>} IDs na ordem desejada.
|
|
549
|
+
*/
|
|
550
|
+
const parseReorderInput = async ({ ownerJid, identifier, rawOrder }) => {
|
|
551
|
+
const tokens = String(rawOrder || '')
|
|
552
|
+
.split(/[\s,]+/)
|
|
553
|
+
.map((item) => item.trim())
|
|
554
|
+
.filter(Boolean);
|
|
555
|
+
|
|
556
|
+
if (!tokens.length) {
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const onlyNumbers = tokens.every((token) => /^\d+$/.test(token));
|
|
561
|
+
if (!onlyNumbers) {
|
|
562
|
+
return tokens;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const pack = await stickerPackService.getPackInfo({ ownerJid, identifier });
|
|
566
|
+
const ids = [];
|
|
567
|
+
|
|
568
|
+
for (const token of tokens) {
|
|
569
|
+
const index = Number(token);
|
|
570
|
+
const item = pack.items[index - 1];
|
|
571
|
+
if (item?.sticker_id) ids.push(item.sticker_id);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return ids;
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Resolve sticker a partir do contexto atual (quoted/último sticker).
|
|
579
|
+
*
|
|
580
|
+
* @param {{ messageInfo: object, ownerJid: string, includeQuoted?: boolean }} params Contexto da mensagem.
|
|
581
|
+
* @returns {Promise<object|null>} Asset resolvido.
|
|
582
|
+
*/
|
|
583
|
+
const resolveStickerFromCommandContext = async ({ messageInfo, ownerJid, includeQuoted = true }) => {
|
|
584
|
+
return resolveStickerAssetForCommand({
|
|
585
|
+
messageInfo,
|
|
586
|
+
ownerJid,
|
|
587
|
+
includeQuoted,
|
|
588
|
+
fallbackToLast: true,
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Handler principal do comando `pack` e seus subcomandos.
|
|
594
|
+
*
|
|
595
|
+
* @param {{
|
|
596
|
+
* sock: object,
|
|
597
|
+
* remoteJid: string,
|
|
598
|
+
* messageInfo: object,
|
|
599
|
+
* expirationMessage: number|undefined,
|
|
600
|
+
* senderJid: string,
|
|
601
|
+
* senderName: string,
|
|
602
|
+
* text: string,
|
|
603
|
+
* commandPrefix: string,
|
|
604
|
+
* }} params Contexto da requisição.
|
|
605
|
+
* @returns {Promise<void>}
|
|
606
|
+
*/
|
|
607
|
+
export async function handlePackCommand({
|
|
608
|
+
sock,
|
|
609
|
+
remoteJid,
|
|
610
|
+
messageInfo,
|
|
611
|
+
expirationMessage,
|
|
612
|
+
senderJid,
|
|
613
|
+
senderName,
|
|
614
|
+
text,
|
|
615
|
+
commandPrefix,
|
|
616
|
+
}) {
|
|
617
|
+
const ownerJid = senderJid;
|
|
618
|
+
const rate = checkRateLimit(ownerJid);
|
|
619
|
+
|
|
620
|
+
if (rate.limited) {
|
|
621
|
+
const waitSeconds = Math.ceil(rate.remainingMs / 1000);
|
|
622
|
+
await sendReply({
|
|
623
|
+
sock,
|
|
624
|
+
remoteJid,
|
|
625
|
+
messageInfo,
|
|
626
|
+
expirationMessage,
|
|
627
|
+
text: buildActionMessage({
|
|
628
|
+
title: '⏳ *Muitas ações em sequência.*',
|
|
629
|
+
explanation: ['Para manter o sistema estável, ativei uma pausa rápida antes do próximo comando.'],
|
|
630
|
+
details: [`⏱️ Você poderá tentar novamente em: *${waitSeconds}s*.`],
|
|
631
|
+
nextSteps: ['Aguarde o tempo acima e repita o comando de pack que deseja executar.'],
|
|
632
|
+
}),
|
|
633
|
+
});
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const { command, rest } = extractCommandParts(text);
|
|
638
|
+
const subcommand = command || 'help';
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
switch (subcommand) {
|
|
642
|
+
case 'create': {
|
|
643
|
+
const segments = splitPipeSegments(rest);
|
|
644
|
+
const base = segments.shift() || '';
|
|
645
|
+
const options = parsePipeOptions(segments);
|
|
646
|
+
|
|
647
|
+
const name = normalizePackName(base);
|
|
648
|
+
const publisher = options.publisher || options.pub || options.autor || senderName || 'OmniZap';
|
|
649
|
+
const description = options.desc || options.description || '';
|
|
650
|
+
const visibility = options.visibility || options.vis || 'public';
|
|
651
|
+
|
|
652
|
+
const created = await stickerPackService.createPack({
|
|
653
|
+
ownerJid,
|
|
654
|
+
name,
|
|
655
|
+
publisher,
|
|
656
|
+
description,
|
|
657
|
+
visibility,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
await sendReply({
|
|
661
|
+
sock,
|
|
662
|
+
remoteJid,
|
|
663
|
+
messageInfo,
|
|
664
|
+
expirationMessage,
|
|
665
|
+
text: buildActionMessage({
|
|
666
|
+
title: '✅ *Pack criado!*',
|
|
667
|
+
explanation: ['Seu pack já está disponível e pronto para receber figurinhas.'],
|
|
668
|
+
details: [
|
|
669
|
+
`📛 Nome: *${created.name}*`,
|
|
670
|
+
`🆔 ID: \`${created.pack_key}\``,
|
|
671
|
+
`👤 Publisher: *${created.publisher}*`,
|
|
672
|
+
`👁️ Visibilidade: ${formatVisibilityLabel(created.visibility)}`,
|
|
673
|
+
],
|
|
674
|
+
nextSteps: [
|
|
675
|
+
`Responda uma figurinha e use: \`${commandPrefix}pack add ${created.pack_key}\`.`,
|
|
676
|
+
`Para conferir: \`${commandPrefix}pack info ${created.pack_key}\`.`,
|
|
677
|
+
],
|
|
678
|
+
footer: ['💡 Dica: use packs por tema para organizar e enviar mais rápido.'],
|
|
679
|
+
}),
|
|
680
|
+
});
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
case 'list': {
|
|
685
|
+
const packs = await stickerPackService.listPacks({ ownerJid, limit: 100 });
|
|
686
|
+
|
|
687
|
+
await sendReply({
|
|
688
|
+
sock,
|
|
689
|
+
remoteJid,
|
|
690
|
+
messageInfo,
|
|
691
|
+
expirationMessage,
|
|
692
|
+
text: formatPackList(packs, commandPrefix),
|
|
693
|
+
});
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
case 'info': {
|
|
698
|
+
const identifier = readSingleArgument(rest);
|
|
699
|
+
const pack = await stickerPackService.getPackInfo({ ownerJid, identifier });
|
|
700
|
+
|
|
701
|
+
await sendReply({
|
|
702
|
+
sock,
|
|
703
|
+
remoteJid,
|
|
704
|
+
messageInfo,
|
|
705
|
+
expirationMessage,
|
|
706
|
+
text: formatPackInfo(pack, commandPrefix),
|
|
707
|
+
});
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
case 'rename': {
|
|
712
|
+
const { identifier, value } = parseIdentifierAndValue(rest);
|
|
713
|
+
const normalizedName = normalizePackName(value, { label: 'Novo nome do pack' });
|
|
714
|
+
const updated = await stickerPackService.renamePack({ ownerJid, identifier, name: normalizedName });
|
|
715
|
+
|
|
716
|
+
await sendReply({
|
|
717
|
+
sock,
|
|
718
|
+
remoteJid,
|
|
719
|
+
messageInfo,
|
|
720
|
+
expirationMessage,
|
|
721
|
+
text: buildActionMessage({
|
|
722
|
+
title: '✏️ *Nome atualizado!*',
|
|
723
|
+
explanation: ['Alteração salva com sucesso.'],
|
|
724
|
+
details: [`📛 Novo nome: *${updated.name}*`, `🆔 ID: \`${updated.pack_key}\``],
|
|
725
|
+
nextSteps: [`Ver detalhes: \`${commandPrefix}pack info ${updated.pack_key}\`.`],
|
|
726
|
+
}),
|
|
727
|
+
});
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
case 'setpub': {
|
|
732
|
+
const { identifier, value } = parseIdentifierAndValue(rest);
|
|
733
|
+
const updated = await stickerPackService.setPackPublisher({ ownerJid, identifier, publisher: value });
|
|
734
|
+
|
|
735
|
+
await sendReply({
|
|
736
|
+
sock,
|
|
737
|
+
remoteJid,
|
|
738
|
+
messageInfo,
|
|
739
|
+
expirationMessage,
|
|
740
|
+
text: buildActionMessage({
|
|
741
|
+
title: '👤 *Publisher atualizado!*',
|
|
742
|
+
explanation: ['O publisher deste pack foi ajustado e já aparece nas informações.'],
|
|
743
|
+
details: [`📦 Pack: *${updated.name}*`, `👤 Publisher: *${updated.publisher}*`, `🆔 ID: \`${updated.pack_key}\``],
|
|
744
|
+
nextSteps: [
|
|
745
|
+
`Se quiser, ajuste a descrição: \`${commandPrefix}pack setdesc ${updated.pack_key} "Nova descrição"\`.`,
|
|
746
|
+
],
|
|
747
|
+
}),
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
case 'setdesc': {
|
|
753
|
+
const { identifier, value } = parseIdentifierAndValue(rest);
|
|
754
|
+
const description = value === '-' || value.toLowerCase() === 'clear' ? '' : value;
|
|
755
|
+
const updated = await stickerPackService.setPackDescription({ ownerJid, identifier, description });
|
|
756
|
+
|
|
757
|
+
await sendReply({
|
|
758
|
+
sock,
|
|
759
|
+
remoteJid,
|
|
760
|
+
messageInfo,
|
|
761
|
+
expirationMessage,
|
|
762
|
+
text: buildActionMessage({
|
|
763
|
+
title: '📝 *Descrição atualizada!*',
|
|
764
|
+
explanation: ['A descrição ajuda a identificar o tema do pack.'],
|
|
765
|
+
details: [
|
|
766
|
+
`📦 Pack: *${updated.name}*`,
|
|
767
|
+
description ? `📝 Descrição: "${updated.description}"` : '🧹 Descrição removida.',
|
|
768
|
+
],
|
|
769
|
+
nextSteps: [`Ver como ficou: \`${commandPrefix}pack info ${updated.pack_key}\`.`],
|
|
770
|
+
}),
|
|
771
|
+
});
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
case 'setcover': {
|
|
776
|
+
const identifier = readSingleArgument(rest);
|
|
777
|
+
const asset = await resolveStickerFromCommandContext({ messageInfo, ownerJid });
|
|
778
|
+
|
|
779
|
+
if (!asset) {
|
|
780
|
+
throw new StickerPackError(
|
|
781
|
+
STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND,
|
|
782
|
+
'Não encontrei uma figurinha para definir como capa.',
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const updated = await stickerPackService.setPackCover({
|
|
787
|
+
ownerJid,
|
|
788
|
+
identifier,
|
|
789
|
+
stickerId: asset.id,
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
await sendReply({
|
|
793
|
+
sock,
|
|
794
|
+
remoteJid,
|
|
795
|
+
messageInfo,
|
|
796
|
+
expirationMessage,
|
|
797
|
+
text: buildActionMessage({
|
|
798
|
+
title: '🖼️ *Capa definida!*',
|
|
799
|
+
explanation: ['A figurinha selecionada agora é a capa deste pack.'],
|
|
800
|
+
details: [`📦 Pack: *${updated.name}*`, `🆔 ID: \`${updated.pack_key}\``],
|
|
801
|
+
nextSteps: [`Para enviar: \`${commandPrefix}pack send ${updated.pack_key}\`.`],
|
|
802
|
+
}),
|
|
803
|
+
});
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
case 'add': {
|
|
808
|
+
const segments = splitPipeSegments(rest);
|
|
809
|
+
const identifier = readSingleArgument(segments.shift() || '');
|
|
810
|
+
const options = parsePipeOptions(segments);
|
|
811
|
+
|
|
812
|
+
const asset = await resolveStickerFromCommandContext({ messageInfo, ownerJid });
|
|
813
|
+
if (!asset) {
|
|
814
|
+
throw new StickerPackError(
|
|
815
|
+
STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND,
|
|
816
|
+
'Não encontrei uma figurinha para adicionar.',
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const updated = await stickerPackService.addStickerToPack({
|
|
821
|
+
ownerJid,
|
|
822
|
+
identifier,
|
|
823
|
+
asset,
|
|
824
|
+
emojis: options.emojis,
|
|
825
|
+
accessibilityLabel: options.label || options.accessibility || null,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
await sendReply({
|
|
829
|
+
sock,
|
|
830
|
+
remoteJid,
|
|
831
|
+
messageInfo,
|
|
832
|
+
expirationMessage,
|
|
833
|
+
text: buildActionMessage({
|
|
834
|
+
title: '➕ *Figurinha adicionada!*',
|
|
835
|
+
explanation: ['Item adicionado com sucesso ao pack selecionado.'],
|
|
836
|
+
details: [
|
|
837
|
+
`📦 Pack: *${updated.name}*`,
|
|
838
|
+
`🧩 Itens: *${updated.items.length}/${MAX_PACK_ITEMS}*`,
|
|
839
|
+
`🆔 ID: \`${updated.pack_key}\``,
|
|
840
|
+
],
|
|
841
|
+
nextSteps: [
|
|
842
|
+
`Definir como capa: responda a figurinha e use \`${commandPrefix}pack setcover ${updated.pack_key}\`.`,
|
|
843
|
+
`Ver lista completa: \`${commandPrefix}pack info ${updated.pack_key}\`.`,
|
|
844
|
+
],
|
|
845
|
+
}),
|
|
846
|
+
});
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
case 'remove': {
|
|
851
|
+
const { token: identifier, rest: selectorRest } = readToken(rest);
|
|
852
|
+
const { token: selector } = readToken(selectorRest);
|
|
853
|
+
|
|
854
|
+
const result = await stickerPackService.removeStickerFromPack({
|
|
855
|
+
ownerJid,
|
|
856
|
+
identifier,
|
|
857
|
+
selector,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
await sendReply({
|
|
861
|
+
sock,
|
|
862
|
+
remoteJid,
|
|
863
|
+
messageInfo,
|
|
864
|
+
expirationMessage,
|
|
865
|
+
text: buildActionMessage({
|
|
866
|
+
title: '🗑️ *Figurinha removida!*',
|
|
867
|
+
explanation: ['Remoção concluída e o pack foi reordenado automaticamente.'],
|
|
868
|
+
details: [
|
|
869
|
+
`📦 Pack: *${result.pack.name}*`,
|
|
870
|
+
`🔢 Item removido: figurinha #${result.removed.position}`,
|
|
871
|
+
`🧩 Itens: *${result.pack.items.length}/${MAX_PACK_ITEMS}*`,
|
|
872
|
+
],
|
|
873
|
+
nextSteps: [`Conferir: \`${commandPrefix}pack info ${result.pack.pack_key}\`.`],
|
|
874
|
+
}),
|
|
875
|
+
});
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
case 'reorder': {
|
|
880
|
+
const { token: identifier, rest: rawOrder } = readToken(rest);
|
|
881
|
+
const orderStickerIds = await parseReorderInput({
|
|
882
|
+
ownerJid,
|
|
883
|
+
identifier,
|
|
884
|
+
rawOrder,
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
const updated = await stickerPackService.reorderPackItems({
|
|
888
|
+
ownerJid,
|
|
889
|
+
identifier,
|
|
890
|
+
orderStickerIds,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
await sendReply({
|
|
894
|
+
sock,
|
|
895
|
+
remoteJid,
|
|
896
|
+
messageInfo,
|
|
897
|
+
expirationMessage,
|
|
898
|
+
text: buildActionMessage({
|
|
899
|
+
title: '🔀 *Ordem atualizada!*',
|
|
900
|
+
explanation: ['A nova sequência foi aplicada ao pack.'],
|
|
901
|
+
details: [`📦 Pack: *${updated.name}*`, `🆔 ID: \`${updated.pack_key}\``],
|
|
902
|
+
nextSteps: [`Verificar sequência: \`${commandPrefix}pack info ${updated.pack_key}\`.`],
|
|
903
|
+
}),
|
|
904
|
+
});
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
case 'clone': {
|
|
909
|
+
const { token: identifier, rest: cloneNameRaw } = readToken(rest);
|
|
910
|
+
const cloneName = normalizePackName(cloneNameRaw, { label: 'Novo nome do clone' });
|
|
911
|
+
|
|
912
|
+
const cloned = await stickerPackService.clonePack({
|
|
913
|
+
ownerJid,
|
|
914
|
+
identifier,
|
|
915
|
+
newName: cloneName,
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
await sendReply({
|
|
919
|
+
sock,
|
|
920
|
+
remoteJid,
|
|
921
|
+
messageInfo,
|
|
922
|
+
expirationMessage,
|
|
923
|
+
text: buildActionMessage({
|
|
924
|
+
title: '🧬 *Clone criado!*',
|
|
925
|
+
explanation: ['O pack foi duplicado com as mesmas figurinhas e configurações.'],
|
|
926
|
+
details: [`📦 Novo pack: *${cloned.name}*`, `🆔 ID: \`${cloned.pack_key}\``],
|
|
927
|
+
nextSteps: [
|
|
928
|
+
`Renomear: \`${commandPrefix}pack rename ${cloned.pack_key} novonome\`.`,
|
|
929
|
+
`Enviar: \`${commandPrefix}pack send ${cloned.pack_key}\`.`,
|
|
930
|
+
],
|
|
931
|
+
}),
|
|
932
|
+
});
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
case 'delete': {
|
|
937
|
+
const identifier = readSingleArgument(rest);
|
|
938
|
+
const deleted = await stickerPackService.deletePack({ ownerJid, identifier });
|
|
939
|
+
|
|
940
|
+
await sendReply({
|
|
941
|
+
sock,
|
|
942
|
+
remoteJid,
|
|
943
|
+
messageInfo,
|
|
944
|
+
expirationMessage,
|
|
945
|
+
text: buildActionMessage({
|
|
946
|
+
title: '🗑️ *Pack removido!*',
|
|
947
|
+
explanation: ['O pack foi excluído e não aparecerá mais na sua lista.'],
|
|
948
|
+
details: [`📦 Pack removido: *${deleted.name}*`],
|
|
949
|
+
nextSteps: [`Criar outro: \`${commandPrefix}pack create meupack\`.`],
|
|
950
|
+
}),
|
|
951
|
+
});
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
case 'publish': {
|
|
956
|
+
const { token: identifier, rest: visibilityRaw } = readToken(rest);
|
|
957
|
+
const visibility = unquote(visibilityRaw);
|
|
958
|
+
|
|
959
|
+
const updated = await stickerPackService.setPackVisibility({
|
|
960
|
+
ownerJid,
|
|
961
|
+
identifier,
|
|
962
|
+
visibility,
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
await sendReply({
|
|
966
|
+
sock,
|
|
967
|
+
remoteJid,
|
|
968
|
+
messageInfo,
|
|
969
|
+
expirationMessage,
|
|
970
|
+
text: buildActionMessage({
|
|
971
|
+
title: '🌐 *Visibilidade atualizada!*',
|
|
972
|
+
explanation: ['A configuração de privacidade foi aplicada ao pack.'],
|
|
973
|
+
details: [
|
|
974
|
+
`📦 Pack: *${updated.name}*`,
|
|
975
|
+
`👁️ Visibilidade: ${formatVisibilityLabel(updated.visibility)}`,
|
|
976
|
+
`🆔 ID: \`${updated.pack_key}\``,
|
|
977
|
+
],
|
|
978
|
+
nextSteps: [`Compartilhar/enviar: \`${commandPrefix}pack send ${updated.pack_key}\`.`],
|
|
979
|
+
}),
|
|
980
|
+
});
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
case 'send': {
|
|
985
|
+
const identifier = readSingleArgument(rest);
|
|
986
|
+
const packDetails = await stickerPackService.getPackInfoForSend({ ownerJid, identifier });
|
|
987
|
+
const packBuild = await buildStickerPackMessage(packDetails);
|
|
988
|
+
const sendResult = await sendStickerPackWithFallback({
|
|
989
|
+
sock,
|
|
990
|
+
jid: remoteJid,
|
|
991
|
+
messageInfo,
|
|
992
|
+
expirationMessage,
|
|
993
|
+
packBuild,
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
if (sendResult.mode === 'native') {
|
|
997
|
+
await sendReply({
|
|
998
|
+
sock,
|
|
999
|
+
remoteJid,
|
|
1000
|
+
messageInfo,
|
|
1001
|
+
expirationMessage,
|
|
1002
|
+
text: buildActionMessage({
|
|
1003
|
+
title: '📤 *Pack enviado!*',
|
|
1004
|
+
explanation: ['Enviei no formato nativo (melhor experiência e compatibilidade).'],
|
|
1005
|
+
details: [
|
|
1006
|
+
`📦 Pack: *${packDetails.name}*`,
|
|
1007
|
+
`🆔 ID: \`${packDetails.pack_key}\``,
|
|
1008
|
+
`🧩 Enviadas: *${sendResult.sentCount} figurinha(s)*`,
|
|
1009
|
+
],
|
|
1010
|
+
nextSteps: [
|
|
1011
|
+
`Ver detalhes: \`${commandPrefix}pack info ${packDetails.pack_key}\`.`,
|
|
1012
|
+
`Editar: \`${commandPrefix}pack add ${packDetails.pack_key}\` ou \`${commandPrefix}pack remove ${packDetails.pack_key} <item>\`.`,
|
|
1013
|
+
],
|
|
1014
|
+
}),
|
|
1015
|
+
});
|
|
1016
|
+
} else {
|
|
1017
|
+
await sendReply({
|
|
1018
|
+
sock,
|
|
1019
|
+
remoteJid,
|
|
1020
|
+
messageInfo,
|
|
1021
|
+
expirationMessage,
|
|
1022
|
+
text: buildActionMessage({
|
|
1023
|
+
title: 'ℹ️ *Pack enviado em modo compatível.*',
|
|
1024
|
+
explanation: [
|
|
1025
|
+
`O cliente não aceitou o formato nativo para *${packDetails.name}*.`,
|
|
1026
|
+
'Enviei em modo compatível (prévia + figurinhas individuais).',
|
|
1027
|
+
],
|
|
1028
|
+
details: [
|
|
1029
|
+
`📦 Pack: *${packDetails.name}*`,
|
|
1030
|
+
`🧩 Progresso: *${sendResult.sentCount}/${sendResult.total}*`,
|
|
1031
|
+
sendResult.nativeError ? `🛠 Detalhe técnico: ${sendResult.nativeError}` : null,
|
|
1032
|
+
],
|
|
1033
|
+
nextSteps: [
|
|
1034
|
+
`Você pode continuar gerenciando: \`${commandPrefix}pack info ${packDetails.pack_key}\`.`,
|
|
1035
|
+
`Para tentar novamente no formato nativo: \`${commandPrefix}pack send ${packDetails.pack_key}\` mais tarde.`,
|
|
1036
|
+
],
|
|
1037
|
+
}),
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
default: {
|
|
1044
|
+
await sendReply({
|
|
1045
|
+
sock,
|
|
1046
|
+
remoteJid,
|
|
1047
|
+
messageInfo,
|
|
1048
|
+
expirationMessage,
|
|
1049
|
+
text: buildPackHelp(commandPrefix),
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
logger.error('Erro ao processar comando de sticker pack.', {
|
|
1055
|
+
action: 'pack_command_error',
|
|
1056
|
+
subcommand,
|
|
1057
|
+
owner_jid: ownerJid,
|
|
1058
|
+
error: error.message,
|
|
1059
|
+
code: error.code,
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
await sendReply({
|
|
1063
|
+
sock,
|
|
1064
|
+
remoteJid,
|
|
1065
|
+
messageInfo,
|
|
1066
|
+
expirationMessage,
|
|
1067
|
+
text: formatErrorMessage(error, commandPrefix),
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Captura stickers recebidos para manter cache/storage atualizado.
|
|
1074
|
+
*
|
|
1075
|
+
* @param {{ messageInfo: object, senderJid: string, isMessageFromBot: boolean }} params Contexto da mensagem recebida.
|
|
1076
|
+
* @returns {Promise<object|null>} Asset capturado ou `null`.
|
|
1077
|
+
*/
|
|
1078
|
+
export async function maybeCaptureIncomingSticker({ messageInfo, senderJid, isMessageFromBot }) {
|
|
1079
|
+
if (isMessageFromBot) return null;
|
|
1080
|
+
if (!isUserJid(senderJid)) return null;
|
|
1081
|
+
|
|
1082
|
+
try {
|
|
1083
|
+
return await captureIncomingStickerAsset({
|
|
1084
|
+
messageInfo,
|
|
1085
|
+
ownerJid: senderJid,
|
|
1086
|
+
});
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
logger.warn('Falha ao capturar figurinha recebida para storage.', {
|
|
1089
|
+
action: 'pack_capture_warning',
|
|
1090
|
+
owner_jid: senderJid,
|
|
1091
|
+
error: error.message,
|
|
1092
|
+
});
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|