@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,51 @@
|
|
|
1
|
+
import { createStickerPackService } from './stickerPackService.js';
|
|
2
|
+
import {
|
|
3
|
+
bumpStickerPackVersion,
|
|
4
|
+
createStickerPack,
|
|
5
|
+
ensureUniquePackKey,
|
|
6
|
+
findStickerPackByPackKey,
|
|
7
|
+
findStickerPackByOwnerAndIdentifier,
|
|
8
|
+
listStickerPacksByOwner,
|
|
9
|
+
softDeleteStickerPack,
|
|
10
|
+
updateStickerPackFields,
|
|
11
|
+
} from './stickerPackRepository.js';
|
|
12
|
+
import {
|
|
13
|
+
bulkUpdateStickerPackPositions,
|
|
14
|
+
countStickerPackItems,
|
|
15
|
+
createStickerPackItem,
|
|
16
|
+
getMaxStickerPackPosition,
|
|
17
|
+
getStickerPackItemByPosition,
|
|
18
|
+
getStickerPackItemByStickerId,
|
|
19
|
+
listStickerPackItems,
|
|
20
|
+
removeStickerPackItemByStickerId,
|
|
21
|
+
shiftStickerPackPositionsAfter,
|
|
22
|
+
} from './stickerPackItemRepository.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Serviço principal de sticker pack com dependências concretas de runtime.
|
|
26
|
+
*/
|
|
27
|
+
const stickerPackService = createStickerPackService({
|
|
28
|
+
packRepository: {
|
|
29
|
+
createStickerPack,
|
|
30
|
+
listStickerPacksByOwner,
|
|
31
|
+
findStickerPackByOwnerAndIdentifier,
|
|
32
|
+
findStickerPackByPackKey,
|
|
33
|
+
updateStickerPackFields,
|
|
34
|
+
softDeleteStickerPack,
|
|
35
|
+
ensureUniquePackKey,
|
|
36
|
+
bumpStickerPackVersion,
|
|
37
|
+
},
|
|
38
|
+
itemRepository: {
|
|
39
|
+
listStickerPackItems,
|
|
40
|
+
countStickerPackItems,
|
|
41
|
+
getMaxStickerPackPosition,
|
|
42
|
+
createStickerPackItem,
|
|
43
|
+
getStickerPackItemByStickerId,
|
|
44
|
+
getStickerPackItemByPosition,
|
|
45
|
+
removeStickerPackItemByStickerId,
|
|
46
|
+
shiftStickerPackPositionsAfter,
|
|
47
|
+
bulkUpdateStickerPackPositions,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export default stickerPackService;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { normalizeJid } from '../../config/baileysConfig.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Utilitários compartilhados para normalização e sanitização de dados de packs.
|
|
6
|
+
*/
|
|
7
|
+
const removeControlChars = (value) =>
|
|
8
|
+
Array.from(String(value ?? ''))
|
|
9
|
+
.filter((char) => {
|
|
10
|
+
const code = char.charCodeAt(0);
|
|
11
|
+
return !(code <= 31 || code === 127);
|
|
12
|
+
})
|
|
13
|
+
.join('');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sanitiza texto removendo caracteres de controle, espaços duplicados e limite de tamanho.
|
|
17
|
+
*
|
|
18
|
+
* @param {unknown} value Valor recebido na entrada.
|
|
19
|
+
* @param {number} maxLength Limite máximo de caracteres.
|
|
20
|
+
* @param {{ allowEmpty?: boolean }} [options] Configurações de sanitização.
|
|
21
|
+
* @returns {string|null} Texto sanitizado ou `null` quando vazio e não permitido.
|
|
22
|
+
*/
|
|
23
|
+
export const sanitizeText = (value, maxLength, { allowEmpty = false } = {}) => {
|
|
24
|
+
const normalized = removeControlChars(value).replace(/\s+/g, ' ').trim();
|
|
25
|
+
|
|
26
|
+
const sliced = maxLength ? normalized.slice(0, maxLength) : normalized;
|
|
27
|
+
if (!sliced && !allowEmpty) return null;
|
|
28
|
+
return sliced;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Converte texto para formato slug compatível com IDs legíveis.
|
|
33
|
+
*
|
|
34
|
+
* @param {unknown} value Texto de origem.
|
|
35
|
+
* @param {{ fallback?: string, maxLength?: number }} [options] Fallback e tamanho máximo.
|
|
36
|
+
* @returns {string} Slug normalizado.
|
|
37
|
+
*/
|
|
38
|
+
export const slugify = (value, { fallback = 'pack', maxLength = 32 } = {}) => {
|
|
39
|
+
const normalized = String(value ?? '')
|
|
40
|
+
.normalize('NFD')
|
|
41
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
44
|
+
.replace(/^-+|-+$/g, '')
|
|
45
|
+
.slice(0, maxLength);
|
|
46
|
+
|
|
47
|
+
return normalized || fallback;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Gera hash SHA-256 curto para identificação estável.
|
|
52
|
+
*
|
|
53
|
+
* @param {unknown} value Valor base para o hash.
|
|
54
|
+
* @param {number} [size=8] Quantidade de caracteres retornados.
|
|
55
|
+
* @returns {string} Hash hexadecimal truncado.
|
|
56
|
+
*/
|
|
57
|
+
export const shortHash = (value, size = 8) => createHash('sha256').update(String(value ?? '')).digest('hex').slice(0, size);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normaliza o JID do dono mantendo fallback para o valor original.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} jid JID do usuário.
|
|
63
|
+
* @returns {string} JID normalizado.
|
|
64
|
+
*/
|
|
65
|
+
export const normalizeOwnerJid = (jid) => normalizeJid(jid || '') || jid || '';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Normaliza visibilidade para os valores aceitos pelo módulo.
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} value Valor recebido.
|
|
71
|
+
* @param {'private'|'public'|'unlisted'|null} [fallback='private'] Valor de fallback.
|
|
72
|
+
* @returns {'private'|'public'|'unlisted'|null} Valor de visibilidade válido.
|
|
73
|
+
*/
|
|
74
|
+
export const toVisibility = (value, fallback = 'private') => {
|
|
75
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
76
|
+
if (normalized === 'public' || normalized === 'unlisted' || normalized === 'private') {
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
return fallback;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Converte string ou lista em array de emojis limitado a 8 itens.
|
|
84
|
+
*
|
|
85
|
+
* @param {string|string[]|unknown} value Emojis em string CSV ou array.
|
|
86
|
+
* @returns {string[]} Lista normalizada de emojis.
|
|
87
|
+
*/
|
|
88
|
+
export const parseEmojiList = (value) => {
|
|
89
|
+
if (!value) return [];
|
|
90
|
+
if (Array.isArray(value)) return value.map((item) => String(item)).filter(Boolean).slice(0, 8);
|
|
91
|
+
|
|
92
|
+
return String(value)
|
|
93
|
+
.split(',')
|
|
94
|
+
.map((item) => item.trim())
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.slice(0, 8);
|
|
97
|
+
};
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
6
|
+
import { downloadMediaMessage, extractMediaDetails } from '../../config/baileysConfig.js';
|
|
7
|
+
import {
|
|
8
|
+
createStickerAsset,
|
|
9
|
+
findLatestStickerAssetByOwner,
|
|
10
|
+
findStickerAssetById,
|
|
11
|
+
findStickerAssetBySha256,
|
|
12
|
+
updateStickerAssetStoragePath,
|
|
13
|
+
} from './stickerAssetRepository.js';
|
|
14
|
+
import { ensureStickerAssetClassified } from './stickerClassificationService.js';
|
|
15
|
+
import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
|
|
16
|
+
import { normalizeOwnerJid } from './stickerPackUtils.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Camada de storage local para assets de figurinha do sistema de packs.
|
|
20
|
+
*/
|
|
21
|
+
const STORAGE_ROOT = path.resolve(process.env.STICKER_STORAGE_DIR || path.join(process.cwd(), 'data', 'stickers'));
|
|
22
|
+
const TEMP_ROOT = path.join(process.cwd(), 'temp', 'sticker-pack-assets');
|
|
23
|
+
const DEFAULT_MAX_STICKER_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
const MAX_STICKER_BYTES = Math.max(64 * 1024, Number(process.env.STICKER_PACK_MAX_STICKER_BYTES) || DEFAULT_MAX_STICKER_BYTES);
|
|
25
|
+
const LAST_STICKER_TTL_MS = Math.max(60_000, Number(process.env.STICKER_PACK_LAST_CACHE_TTL_MS) || 6 * 60 * 60 * 1000);
|
|
26
|
+
|
|
27
|
+
const lastStickerCache = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Converte JID para um token seguro no caminho de disco.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} ownerJid JID do usuário.
|
|
33
|
+
* @returns {string} Token seguro para diretório.
|
|
34
|
+
*/
|
|
35
|
+
const safeOwnerToken = (ownerJid) => {
|
|
36
|
+
const normalized = normalizeOwnerJid(ownerJid);
|
|
37
|
+
const token = String(normalized || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 100);
|
|
38
|
+
return token || 'unknown';
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const buildStoragePath = (ownerJid, sha256) => path.join(STORAGE_ROOT, safeOwnerToken(ownerJid), `${sha256}.webp`);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Verifica se um caminho existe no sistema de arquivos.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} targetPath Caminho absoluto do arquivo.
|
|
47
|
+
* @returns {Promise<boolean>} `true` quando o arquivo existe.
|
|
48
|
+
*/
|
|
49
|
+
const fileExists = async (targetPath) => {
|
|
50
|
+
try {
|
|
51
|
+
await fs.access(targetPath);
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Faz validação rápida de assinatura WEBP.
|
|
60
|
+
*
|
|
61
|
+
* @param {Buffer} buffer Buffer da mídia.
|
|
62
|
+
* @returns {boolean} `true` quando parece WEBP.
|
|
63
|
+
*/
|
|
64
|
+
const isLikelyWebp = (buffer) => {
|
|
65
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 16) return false;
|
|
66
|
+
return buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP';
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detecta chunks de animação em WEBP.
|
|
71
|
+
*
|
|
72
|
+
* @param {Buffer} buffer Buffer da mídia.
|
|
73
|
+
* @returns {boolean} `true` quando o sticker é animado.
|
|
74
|
+
*/
|
|
75
|
+
const detectAnimatedWebp = (buffer) => {
|
|
76
|
+
if (!Buffer.isBuffer(buffer)) return false;
|
|
77
|
+
return buffer.includes(Buffer.from('ANIM')) || buffer.includes(Buffer.from('ANMF'));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Lê dimensões de um arquivo WEBP a partir dos headers.
|
|
82
|
+
*
|
|
83
|
+
* @param {Buffer} buffer Buffer do sticker.
|
|
84
|
+
* @returns {{ width: number|null, height: number|null }} Dimensões detectadas.
|
|
85
|
+
*/
|
|
86
|
+
const parseWebpDimensions = (buffer) => {
|
|
87
|
+
if (!isLikelyWebp(buffer)) return { width: null, height: null };
|
|
88
|
+
|
|
89
|
+
const chunk = buffer.subarray(12, 16).toString('ascii');
|
|
90
|
+
|
|
91
|
+
if (chunk === 'VP8X' && buffer.length >= 30) {
|
|
92
|
+
const width = 1 + buffer.readUIntLE(24, 3);
|
|
93
|
+
const height = 1 + buffer.readUIntLE(27, 3);
|
|
94
|
+
return { width, height };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (chunk === 'VP8 ' && buffer.length >= 30) {
|
|
98
|
+
const width = buffer.readUInt16LE(26) & 0x3fff;
|
|
99
|
+
const height = buffer.readUInt16LE(28) & 0x3fff;
|
|
100
|
+
return {
|
|
101
|
+
width: width > 0 ? width : null,
|
|
102
|
+
height: height > 0 ? height : null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (chunk === 'VP8L' && buffer.length >= 25) {
|
|
107
|
+
const b0 = buffer[21];
|
|
108
|
+
const b1 = buffer[22];
|
|
109
|
+
const b2 = buffer[23];
|
|
110
|
+
const b3 = buffer[24];
|
|
111
|
+
|
|
112
|
+
const width = 1 + (((b1 & 0x3f) << 8) | b0);
|
|
113
|
+
const height = 1 + (((b3 & 0x0f) << 10) | (b2 << 2) | ((b1 & 0xc0) >> 6));
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
width: width > 0 ? width : null,
|
|
117
|
+
height: height > 0 ? height : null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { width: null, height: null };
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Memoriza o último sticker salvo por dono para fallback rápido.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} ownerJid JID do dono.
|
|
128
|
+
* @param {string} assetId ID do asset.
|
|
129
|
+
* @returns {void}
|
|
130
|
+
*/
|
|
131
|
+
const rememberLastSticker = (ownerJid, assetId) => {
|
|
132
|
+
if (!ownerJid || !assetId) return;
|
|
133
|
+
|
|
134
|
+
const normalized = normalizeOwnerJid(ownerJid);
|
|
135
|
+
lastStickerCache.set(normalized, {
|
|
136
|
+
assetId,
|
|
137
|
+
expiresAt: Date.now() + LAST_STICKER_TTL_MS,
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve asset do cache do último sticker (com TTL).
|
|
143
|
+
*
|
|
144
|
+
* @param {string} ownerJid JID do dono.
|
|
145
|
+
* @returns {string|null} ID do asset ainda válido.
|
|
146
|
+
*/
|
|
147
|
+
const resolveLastStickerAssetId = (ownerJid) => {
|
|
148
|
+
const normalized = normalizeOwnerJid(ownerJid);
|
|
149
|
+
const entry = lastStickerCache.get(normalized);
|
|
150
|
+
if (!entry) return null;
|
|
151
|
+
|
|
152
|
+
if (entry.expiresAt <= Date.now()) {
|
|
153
|
+
lastStickerCache.delete(normalized);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return entry.assetId;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Gera hash SHA-256 do buffer.
|
|
162
|
+
*
|
|
163
|
+
* @param {Buffer} buffer Conteúdo da figurinha.
|
|
164
|
+
* @returns {string} Hash SHA-256 em hexadecimal.
|
|
165
|
+
*/
|
|
166
|
+
const computeSha256 = (buffer) => createHash('sha256').update(buffer).digest('hex');
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Garante persistência física do arquivo no storage definitivo.
|
|
170
|
+
*
|
|
171
|
+
* @param {{ ownerJid: string, sha256: string, buffer: Buffer }} params Contexto de escrita.
|
|
172
|
+
* @returns {Promise<string>} Caminho final persistido.
|
|
173
|
+
*/
|
|
174
|
+
const ensureStorageForAsset = async ({ ownerJid, sha256, buffer }) => {
|
|
175
|
+
const targetPath = buildStoragePath(ownerJid, sha256);
|
|
176
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
177
|
+
|
|
178
|
+
if (!(await fileExists(targetPath))) {
|
|
179
|
+
await fs.writeFile(targetPath, buffer);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return targetPath;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Extrai detalhes de mídia sticker de uma mensagem.
|
|
187
|
+
*
|
|
188
|
+
* @param {object} messageInfo Mensagem original.
|
|
189
|
+
* @param {{ includeQuoted?: boolean }} [options] Inclui mídia citada.
|
|
190
|
+
* @returns {object|null} Detalhes da mídia quando for sticker.
|
|
191
|
+
*/
|
|
192
|
+
const resolveStickerMediaDetails = (messageInfo, { includeQuoted = true } = {}) => {
|
|
193
|
+
const mediaDetails = extractMediaDetails(messageInfo, { includeQuoted });
|
|
194
|
+
if (!mediaDetails || mediaDetails.mediaType !== 'sticker') {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return mediaDetails;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Valida buffer recebido antes de salvar no storage.
|
|
203
|
+
*
|
|
204
|
+
* @param {Buffer} buffer Buffer da figurinha.
|
|
205
|
+
* @returns {void}
|
|
206
|
+
* @throws {StickerPackError} Quando o buffer for inválido, grande demais ou não-WEBP.
|
|
207
|
+
*/
|
|
208
|
+
const validateStickerBuffer = (buffer) => {
|
|
209
|
+
if (!Buffer.isBuffer(buffer) || !buffer.length) {
|
|
210
|
+
throw new StickerPackError(STICKER_PACK_ERROR_CODES.STORAGE_ERROR, 'Arquivo da figurinha veio vazio.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (buffer.length > MAX_STICKER_BYTES) {
|
|
214
|
+
throw new StickerPackError(
|
|
215
|
+
STICKER_PACK_ERROR_CODES.INVALID_INPUT,
|
|
216
|
+
`Figurinha excede o limite de ${(MAX_STICKER_BYTES / (1024 * 1024)).toFixed(1)} MB.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!isLikelyWebp(buffer)) {
|
|
221
|
+
throw new StickerPackError(
|
|
222
|
+
STICKER_PACK_ERROR_CODES.INVALID_INPUT,
|
|
223
|
+
'A mídia precisa estar no formato WEBP para entrar no pack.',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Persiste um asset de sticker a partir de buffer em memória.
|
|
230
|
+
*
|
|
231
|
+
* @param {{ ownerJid: string, buffer: Buffer, mimetype?: string }} params Dados de persistência.
|
|
232
|
+
* @returns {Promise<object>} Asset salvo ou já existente.
|
|
233
|
+
*/
|
|
234
|
+
async function persistStickerAssetBuffer({ ownerJid, buffer, mimetype = 'image/webp' }) {
|
|
235
|
+
const normalizedOwner = normalizeOwnerJid(ownerJid);
|
|
236
|
+
validateStickerBuffer(buffer);
|
|
237
|
+
|
|
238
|
+
const sha256 = computeSha256(buffer);
|
|
239
|
+
const existing = await findStickerAssetBySha256(sha256);
|
|
240
|
+
|
|
241
|
+
if (existing) {
|
|
242
|
+
const existingPathOk = existing.storage_path ? await fileExists(existing.storage_path) : false;
|
|
243
|
+
|
|
244
|
+
if (!existingPathOk) {
|
|
245
|
+
const fixedPath = await ensureStorageForAsset({ ownerJid: normalizedOwner, sha256, buffer });
|
|
246
|
+
const repaired = await updateStickerAssetStoragePath(existing.id, fixedPath);
|
|
247
|
+
rememberLastSticker(normalizedOwner, repaired.id);
|
|
248
|
+
await ensureStickerAssetClassified({ asset: repaired, buffer }).catch((error) => {
|
|
249
|
+
logger.warn('Falha ao classificar figurinha reparada durante persistência.', {
|
|
250
|
+
action: 'sticker_asset_classify_repaired_failed',
|
|
251
|
+
asset_id: repaired.id,
|
|
252
|
+
owner_jid: normalizedOwner,
|
|
253
|
+
error: error?.message,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
return repaired;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
rememberLastSticker(normalizedOwner, existing.id);
|
|
260
|
+
await ensureStickerAssetClassified({ asset: existing, buffer }).catch((error) => {
|
|
261
|
+
logger.warn('Falha ao classificar figurinha existente durante persistência.', {
|
|
262
|
+
action: 'sticker_asset_classify_existing_failed',
|
|
263
|
+
asset_id: existing.id,
|
|
264
|
+
owner_jid: normalizedOwner,
|
|
265
|
+
error: error?.message,
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
return existing;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const storagePath = await ensureStorageForAsset({ ownerJid: normalizedOwner, sha256, buffer });
|
|
272
|
+
const { width, height } = parseWebpDimensions(buffer);
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const created = await createStickerAsset({
|
|
276
|
+
id: randomUUID(),
|
|
277
|
+
owner_jid: normalizedOwner,
|
|
278
|
+
sha256,
|
|
279
|
+
mimetype,
|
|
280
|
+
is_animated: detectAnimatedWebp(buffer),
|
|
281
|
+
width,
|
|
282
|
+
height,
|
|
283
|
+
size_bytes: buffer.length,
|
|
284
|
+
storage_path: storagePath,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
rememberLastSticker(normalizedOwner, created.id);
|
|
288
|
+
await ensureStickerAssetClassified({ asset: created, buffer }).catch((error) => {
|
|
289
|
+
logger.warn('Falha ao classificar figurinha recém-criada durante persistência.', {
|
|
290
|
+
action: 'sticker_asset_classify_created_failed',
|
|
291
|
+
asset_id: created.id,
|
|
292
|
+
owner_jid: normalizedOwner,
|
|
293
|
+
error: error?.message,
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
return created;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (error?.code === 'ER_DUP_ENTRY') {
|
|
299
|
+
const duplicated = await findStickerAssetBySha256(sha256);
|
|
300
|
+
if (duplicated) {
|
|
301
|
+
rememberLastSticker(normalizedOwner, duplicated.id);
|
|
302
|
+
return duplicated;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Faz download do sticker da mensagem e delega persistência do buffer.
|
|
311
|
+
*
|
|
312
|
+
* @param {{ mediaDetails: object, ownerJid: string }} params Detalhes da mídia e dono.
|
|
313
|
+
* @returns {Promise<object>} Asset salvo.
|
|
314
|
+
*/
|
|
315
|
+
async function persistStickerAssetFromDetails({ mediaDetails, ownerJid }) {
|
|
316
|
+
const normalizedOwner = normalizeOwnerJid(ownerJid);
|
|
317
|
+
const mediaSize = Number(mediaDetails?.details?.fileLength || mediaDetails?.mediaKey?.fileLength || 0);
|
|
318
|
+
|
|
319
|
+
if (mediaSize > MAX_STICKER_BYTES) {
|
|
320
|
+
throw new StickerPackError(
|
|
321
|
+
STICKER_PACK_ERROR_CODES.INVALID_INPUT,
|
|
322
|
+
`Figurinha excede o limite de ${(MAX_STICKER_BYTES / (1024 * 1024)).toFixed(1)} MB.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const tempOwnerDir = path.join(TEMP_ROOT, safeOwnerToken(normalizedOwner));
|
|
327
|
+
await fs.mkdir(tempOwnerDir, { recursive: true });
|
|
328
|
+
|
|
329
|
+
let downloadedPath = null;
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
downloadedPath = await downloadMediaMessage(mediaDetails.mediaKey, 'sticker', tempOwnerDir);
|
|
333
|
+
if (!downloadedPath) {
|
|
334
|
+
throw new StickerPackError(
|
|
335
|
+
STICKER_PACK_ERROR_CODES.STORAGE_ERROR,
|
|
336
|
+
'Não foi possível baixar a figurinha para armazenamento.',
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const buffer = await fs.readFile(downloadedPath);
|
|
341
|
+
|
|
342
|
+
return await persistStickerAssetBuffer({
|
|
343
|
+
ownerJid: normalizedOwner,
|
|
344
|
+
buffer,
|
|
345
|
+
mimetype: mediaDetails?.details?.mimetype || 'image/webp',
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof StickerPackError) {
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
logger.error('Falha ao persistir figurinha no storage local.', {
|
|
353
|
+
action: 'sticker_asset_store_failed',
|
|
354
|
+
owner_jid: normalizedOwner,
|
|
355
|
+
error: error.message,
|
|
356
|
+
});
|
|
357
|
+
throw new StickerPackError(
|
|
358
|
+
STICKER_PACK_ERROR_CODES.STORAGE_ERROR,
|
|
359
|
+
'Falha ao salvar figurinha no servidor.',
|
|
360
|
+
error,
|
|
361
|
+
);
|
|
362
|
+
} finally {
|
|
363
|
+
if (downloadedPath) {
|
|
364
|
+
await fs.unlink(downloadedPath).catch(() => {});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Salva figurinha direto de um buffer local.
|
|
371
|
+
*
|
|
372
|
+
* @param {{ ownerJid: string, buffer: Buffer, mimetype?: string }} params Dados da figurinha.
|
|
373
|
+
* @returns {Promise<object>} Asset salvo.
|
|
374
|
+
*/
|
|
375
|
+
export async function saveStickerAssetFromBuffer({ ownerJid, buffer, mimetype = 'image/webp' }) {
|
|
376
|
+
const normalizedOwner = normalizeOwnerJid(ownerJid);
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
return await persistStickerAssetBuffer({ ownerJid: normalizedOwner, buffer, mimetype });
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error instanceof StickerPackError) {
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
logger.error('Falha ao persistir figurinha gerada localmente.', {
|
|
386
|
+
action: 'sticker_asset_store_from_buffer_failed',
|
|
387
|
+
owner_jid: normalizedOwner,
|
|
388
|
+
error: error.message,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
throw new StickerPackError(
|
|
392
|
+
STICKER_PACK_ERROR_CODES.STORAGE_ERROR,
|
|
393
|
+
'Falha ao salvar figurinha gerada no servidor.',
|
|
394
|
+
error,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Salva figurinha a partir de mensagem (opcionalmente quoted).
|
|
401
|
+
*
|
|
402
|
+
* @param {{ messageInfo: object, ownerJid: string, includeQuoted?: boolean }} params Contexto da mensagem.
|
|
403
|
+
* @returns {Promise<object|null>} Asset salvo ou `null` quando não há sticker.
|
|
404
|
+
*/
|
|
405
|
+
export async function saveStickerAssetFromMessage({ messageInfo, ownerJid, includeQuoted = true }) {
|
|
406
|
+
const mediaDetails = resolveStickerMediaDetails(messageInfo, { includeQuoted });
|
|
407
|
+
if (!mediaDetails) return null;
|
|
408
|
+
|
|
409
|
+
return persistStickerAssetFromDetails({ mediaDetails, ownerJid });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Captura stickers recebidos no fluxo passivo (sem quoted).
|
|
414
|
+
*
|
|
415
|
+
* @param {{ messageInfo: object, ownerJid: string }} params Contexto da mensagem.
|
|
416
|
+
* @returns {Promise<object|null>} Asset capturado.
|
|
417
|
+
*/
|
|
418
|
+
export async function captureIncomingStickerAsset({ messageInfo, ownerJid }) {
|
|
419
|
+
return saveStickerAssetFromMessage({ messageInfo, ownerJid, includeQuoted: false });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Recupera o último sticker conhecido do usuário via cache+banco.
|
|
424
|
+
*
|
|
425
|
+
* @param {string} ownerJid JID do usuário.
|
|
426
|
+
* @returns {Promise<object|null>} Último asset encontrado.
|
|
427
|
+
*/
|
|
428
|
+
export async function getLastStickerAssetForOwner(ownerJid) {
|
|
429
|
+
const normalizedOwner = normalizeOwnerJid(ownerJid);
|
|
430
|
+
const cachedAssetId = resolveLastStickerAssetId(normalizedOwner);
|
|
431
|
+
|
|
432
|
+
if (cachedAssetId) {
|
|
433
|
+
const cachedAsset = await findStickerAssetById(cachedAssetId);
|
|
434
|
+
if (cachedAsset) return cachedAsset;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const latest = await findLatestStickerAssetByOwner(normalizedOwner);
|
|
438
|
+
if (latest) {
|
|
439
|
+
rememberLastSticker(normalizedOwner, latest.id);
|
|
440
|
+
}
|
|
441
|
+
return latest;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Resolve sticker para comandos: mensagem atual, quoted ou último cacheado.
|
|
446
|
+
*
|
|
447
|
+
* @param {{
|
|
448
|
+
* messageInfo: object,
|
|
449
|
+
* ownerJid: string,
|
|
450
|
+
* includeQuoted?: boolean,
|
|
451
|
+
* fallbackToLast?: boolean,
|
|
452
|
+
* }} params Contexto de resolução.
|
|
453
|
+
* @returns {Promise<object|null>} Asset resolvido.
|
|
454
|
+
*/
|
|
455
|
+
export async function resolveStickerAssetForCommand({
|
|
456
|
+
messageInfo,
|
|
457
|
+
ownerJid,
|
|
458
|
+
includeQuoted = true,
|
|
459
|
+
fallbackToLast = true,
|
|
460
|
+
}) {
|
|
461
|
+
const mediaDetails = resolveStickerMediaDetails(messageInfo, { includeQuoted });
|
|
462
|
+
|
|
463
|
+
if (mediaDetails) {
|
|
464
|
+
return persistStickerAssetFromDetails({ mediaDetails, ownerJid });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!fallbackToLast) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return getLastStickerAssetForOwner(ownerJid);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Lê o arquivo de sticker em disco a partir de um asset.
|
|
476
|
+
*
|
|
477
|
+
* @param {{ storage_path?: string }} asset Registro do asset.
|
|
478
|
+
* @returns {Promise<Buffer>} Buffer da figurinha.
|
|
479
|
+
* @throws {StickerPackError} Quando o arquivo não puder ser lido.
|
|
480
|
+
*/
|
|
481
|
+
export async function readStickerAssetBuffer(asset) {
|
|
482
|
+
if (!asset?.storage_path) {
|
|
483
|
+
throw new StickerPackError(STICKER_PACK_ERROR_CODES.STORAGE_ERROR, 'Caminho do sticker não encontrado no storage.');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
return await fs.readFile(asset.storage_path);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
throw new StickerPackError(
|
|
490
|
+
STICKER_PACK_ERROR_CODES.STORAGE_ERROR,
|
|
491
|
+
`Não foi possível ler a figurinha em disco (${asset.storage_path}).`,
|
|
492
|
+
error,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Retorna configuração efetiva do storage do módulo.
|
|
499
|
+
*
|
|
500
|
+
* @returns {{ storageRoot: string, maxStickerBytes: number }} Configuração ativa.
|
|
501
|
+
*/
|
|
502
|
+
export function getStickerStorageConfig() {
|
|
503
|
+
return {
|
|
504
|
+
storageRoot: STORAGE_ROOT,
|
|
505
|
+
maxStickerBytes: MAX_STICKER_BYTES,
|
|
506
|
+
};
|
|
507
|
+
}
|