@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,788 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
4
|
+
import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
|
|
5
|
+
import { getPackClassificationSummaryByAssetIds } from './stickerClassificationService.js';
|
|
6
|
+
import { normalizeOwnerJid, parseEmojiList, sanitizeText, slugify, toVisibility } from './stickerPackUtils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serviço de domínio para operações de packs e itens de figurinha.
|
|
10
|
+
*/
|
|
11
|
+
const MAX_NAME_LENGTH = 120;
|
|
12
|
+
const MAX_PUBLISHER_LENGTH = 120;
|
|
13
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
14
|
+
const MAX_ACCESSIBILITY_LABEL_LENGTH = 255;
|
|
15
|
+
const DEFAULT_MAX_STICKERS_PER_PACK = Math.max(1, Number(process.env.STICKER_PACK_MAX_ITEMS) || 30);
|
|
16
|
+
const parseMaxPacksPerOwnerLimit = (value, fallback = 50) => {
|
|
17
|
+
if (value === undefined || value === null || value === '') {
|
|
18
|
+
return Math.max(1, Number(fallback) || 50);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalized = String(value).trim().toLowerCase();
|
|
22
|
+
if (['0', '-1', 'inf', 'infinity', 'unlimited', 'sem-limite'].includes(normalized)) {
|
|
23
|
+
return Number.POSITIVE_INFINITY;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parsed = Number(normalized);
|
|
27
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
28
|
+
return Math.max(1, Math.floor(parsed));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Math.max(1, Number(fallback) || 50);
|
|
32
|
+
};
|
|
33
|
+
const DEFAULT_MAX_PACKS_PER_OWNER = parseMaxPacksPerOwnerLimit(process.env.STICKER_PACK_MAX_PACKS_PER_OWNER, 50);
|
|
34
|
+
const PACK_KEY_BASE_MAX_LENGTH = 32;
|
|
35
|
+
const PACK_KEY_SUFFIX_LENGTH = 5;
|
|
36
|
+
const PACK_KEY_SUFFIX_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
37
|
+
const PACK_KEY_MAX_ATTEMPTS = 24;
|
|
38
|
+
const PACK_STATUS_VALUES = new Set(['draft', 'uploading', 'processing', 'published', 'failed']);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {{
|
|
42
|
+
* createPack: Function,
|
|
43
|
+
* listPacks: Function,
|
|
44
|
+
* getPackInfo: Function,
|
|
45
|
+
* getPackInfoForSend: Function,
|
|
46
|
+
* renamePack: Function,
|
|
47
|
+
* setPackPublisher: Function,
|
|
48
|
+
* setPackDescription: Function,
|
|
49
|
+
* setPackVisibility: Function,
|
|
50
|
+
* setPackCover: Function,
|
|
51
|
+
* addStickerToPack: Function,
|
|
52
|
+
* removeStickerFromPack: Function,
|
|
53
|
+
* reorderPackItems: Function,
|
|
54
|
+
* clonePack: Function,
|
|
55
|
+
* deletePack: Function,
|
|
56
|
+
* }} StickerPackService
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
const defaultDependencies = {
|
|
60
|
+
logger,
|
|
61
|
+
packRepository: {},
|
|
62
|
+
itemRepository: {},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const buildError = (code, message, details = null) => new StickerPackError(code, message, details);
|
|
66
|
+
|
|
67
|
+
const ensureValue = (condition, code, message, details = null) => {
|
|
68
|
+
if (!condition) {
|
|
69
|
+
throw buildError(code, message, details);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const normalizePackIdentifier = (identifier) => sanitizeText(identifier, 180, { allowEmpty: false });
|
|
74
|
+
|
|
75
|
+
const normalizeCandidate = (value) => {
|
|
76
|
+
const raw = String(value || '')
|
|
77
|
+
.trim()
|
|
78
|
+
// Remove pontuações de cauda comuns quando o usuário cola links/comandos.
|
|
79
|
+
.replace(/[)\],;.!?]+$/g, '');
|
|
80
|
+
return normalizePackIdentifier(raw);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const extractPackKeyFromValue = (value) => {
|
|
84
|
+
const raw = String(value || '').trim();
|
|
85
|
+
if (!raw) return '';
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const url = new URL(raw, 'https://omnizap.shop');
|
|
89
|
+
const parts = String(url.pathname || '')
|
|
90
|
+
.split('/')
|
|
91
|
+
.map((part) => part.trim())
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
|
|
94
|
+
if (parts[0] && parts[0].toLowerCase() === 'stickers' && parts[1]) {
|
|
95
|
+
return normalizeCandidate(decodeURIComponent(parts[1]));
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Ignora parse de URL inválida e segue para regex.
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const pathMatch = raw.match(/\/stickers\/([^/?#\s]+)/i);
|
|
102
|
+
if (pathMatch?.[1]) {
|
|
103
|
+
return normalizeCandidate(pathMatch[1]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return '';
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const buildIdentifierCandidates = (identifier) => {
|
|
110
|
+
const set = new Set();
|
|
111
|
+
const normalized = normalizeCandidate(identifier);
|
|
112
|
+
if (normalized) set.add(normalized);
|
|
113
|
+
|
|
114
|
+
const fromPath = extractPackKeyFromValue(identifier);
|
|
115
|
+
if (fromPath) set.add(fromPath);
|
|
116
|
+
|
|
117
|
+
for (const current of Array.from(set)) {
|
|
118
|
+
set.add(current.toLowerCase());
|
|
119
|
+
set.add(current.replace(/_/g, '-'));
|
|
120
|
+
set.add(current.replace(/-/g, '_'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return Array.from(set).filter(Boolean);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const isShareableVisibility = (visibility) => ['public', 'unlisted'].includes(String(visibility || '').toLowerCase());
|
|
127
|
+
|
|
128
|
+
const areArraysEqual = (left, right) => {
|
|
129
|
+
if (left.length !== right.length) return false;
|
|
130
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
131
|
+
if (left[index] !== right[index]) return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Executa callback em transação SQL.
|
|
138
|
+
*
|
|
139
|
+
* @param {(connection: import('mysql2/promise').PoolConnection) => Promise<unknown>} handler Função transacional.
|
|
140
|
+
* @returns {Promise<unknown>} Resultado do callback.
|
|
141
|
+
*/
|
|
142
|
+
async function withTransaction(handler) {
|
|
143
|
+
const { pool } = await import('../../../database/index.js');
|
|
144
|
+
const connection = await pool.getConnection();
|
|
145
|
+
try {
|
|
146
|
+
await connection.beginTransaction();
|
|
147
|
+
const result = await handler(connection);
|
|
148
|
+
await connection.commit();
|
|
149
|
+
return result;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
await connection.rollback();
|
|
152
|
+
throw error;
|
|
153
|
+
} finally {
|
|
154
|
+
connection.release();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Cria instância do serviço de sticker pack com dependências injetáveis.
|
|
160
|
+
*
|
|
161
|
+
* @param {{
|
|
162
|
+
* logger?: { info?: Function, error?: Function },
|
|
163
|
+
* packRepository?: Record<string, Function>,
|
|
164
|
+
* itemRepository?: Record<string, Function>,
|
|
165
|
+
* maxStickersPerPack?: number,
|
|
166
|
+
* maxPacksPerOwner?: number,
|
|
167
|
+
* runInTransaction?: Function,
|
|
168
|
+
* }} [options] Configurações e dependências de runtime.
|
|
169
|
+
* @returns {StickerPackService} API de domínio para packs.
|
|
170
|
+
*/
|
|
171
|
+
export function createStickerPackService(options = {}) {
|
|
172
|
+
const deps = {
|
|
173
|
+
...defaultDependencies,
|
|
174
|
+
...options,
|
|
175
|
+
packRepository: {
|
|
176
|
+
...defaultDependencies.packRepository,
|
|
177
|
+
...(options.packRepository || {}),
|
|
178
|
+
},
|
|
179
|
+
itemRepository: {
|
|
180
|
+
...defaultDependencies.itemRepository,
|
|
181
|
+
...(options.itemRepository || {}),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const maxStickersPerPack = Math.max(1, Number(options.maxStickersPerPack) || DEFAULT_MAX_STICKERS_PER_PACK);
|
|
186
|
+
const maxPacksPerOwner = parseMaxPacksPerOwnerLimit(options.maxPacksPerOwner, DEFAULT_MAX_PACKS_PER_OWNER);
|
|
187
|
+
const hasOwnerPackLimit = Number.isFinite(maxPacksPerOwner);
|
|
188
|
+
const runInTransaction = options.runInTransaction || withTransaction;
|
|
189
|
+
|
|
190
|
+
const requiredPackMethods = [
|
|
191
|
+
'createStickerPack',
|
|
192
|
+
'listStickerPacksByOwner',
|
|
193
|
+
'findStickerPackByOwnerAndIdentifier',
|
|
194
|
+
'findStickerPackByPackKey',
|
|
195
|
+
'updateStickerPackFields',
|
|
196
|
+
'softDeleteStickerPack',
|
|
197
|
+
'ensureUniquePackKey',
|
|
198
|
+
'bumpStickerPackVersion',
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const requiredItemMethods = [
|
|
202
|
+
'listStickerPackItems',
|
|
203
|
+
'countStickerPackItems',
|
|
204
|
+
'getMaxStickerPackPosition',
|
|
205
|
+
'createStickerPackItem',
|
|
206
|
+
'getStickerPackItemByStickerId',
|
|
207
|
+
'getStickerPackItemByPosition',
|
|
208
|
+
'removeStickerPackItemByStickerId',
|
|
209
|
+
'shiftStickerPackPositionsAfter',
|
|
210
|
+
'bulkUpdateStickerPackPositions',
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for (const methodName of requiredPackMethods) {
|
|
214
|
+
if (typeof deps.packRepository[methodName] !== 'function') {
|
|
215
|
+
throw new Error(`packRepository.${methodName} é obrigatório para createStickerPackService().`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const methodName of requiredItemMethods) {
|
|
220
|
+
if (typeof deps.itemRepository[methodName] !== 'function') {
|
|
221
|
+
throw new Error(`itemRepository.${methodName} é obrigatório para createStickerPackService().`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const runAction = async (action, context, handler, { expectedErrorCodes = [] } = {}) => {
|
|
226
|
+
const start = process.hrtime.bigint();
|
|
227
|
+
const expectedCodesSet = new Set(
|
|
228
|
+
Array.isArray(expectedErrorCodes)
|
|
229
|
+
? expectedErrorCodes.map((code) => String(code || '').trim()).filter(Boolean)
|
|
230
|
+
: [],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const result = await handler();
|
|
235
|
+
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
236
|
+
|
|
237
|
+
deps.logger.info('Ação de sticker pack concluída.', {
|
|
238
|
+
action: 'sticker_pack_action',
|
|
239
|
+
sticker_action: action,
|
|
240
|
+
duration_ms: Number(durationMs.toFixed(2)),
|
|
241
|
+
...context,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return result;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
247
|
+
const payload = {
|
|
248
|
+
action: 'sticker_pack_action_failed',
|
|
249
|
+
sticker_action: action,
|
|
250
|
+
duration_ms: Number(durationMs.toFixed(2)),
|
|
251
|
+
error: error.message,
|
|
252
|
+
error_code: error.code,
|
|
253
|
+
...context,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (expectedCodesSet.has(String(error?.code || '').trim())) {
|
|
257
|
+
deps.logger.warn('Falha esperada na ação de sticker pack.', {
|
|
258
|
+
...payload,
|
|
259
|
+
expected_failure: true,
|
|
260
|
+
});
|
|
261
|
+
} else {
|
|
262
|
+
deps.logger.error('Falha na ação de sticker pack.', payload);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const ensurePackKey = async (_ownerJid, name, connection = null) => {
|
|
270
|
+
const base = slugify(name, { fallback: 'pack', maxLength: PACK_KEY_BASE_MAX_LENGTH });
|
|
271
|
+
const buildSuffix = () => {
|
|
272
|
+
let suffix = '';
|
|
273
|
+
while (suffix.length < PACK_KEY_SUFFIX_LENGTH) {
|
|
274
|
+
const chunk = randomBytes(PACK_KEY_SUFFIX_LENGTH);
|
|
275
|
+
for (const byte of chunk) {
|
|
276
|
+
// Rejection sampling keeps distribution close to uniform for base-36 chars.
|
|
277
|
+
if (byte >= 252) continue;
|
|
278
|
+
suffix += PACK_KEY_SUFFIX_ALPHABET[byte % PACK_KEY_SUFFIX_ALPHABET.length];
|
|
279
|
+
if (suffix.length >= PACK_KEY_SUFFIX_LENGTH) break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return suffix;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
for (let attempt = 0; attempt < PACK_KEY_MAX_ATTEMPTS; attempt += 1) {
|
|
286
|
+
const candidate = `${base}-${buildSuffix()}`;
|
|
287
|
+
const available = await deps.packRepository.ensureUniquePackKey(candidate, connection);
|
|
288
|
+
if (available) return candidate;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
throw buildError(STICKER_PACK_ERROR_CODES.INTERNAL_ERROR, 'Não foi possível gerar um packId único.');
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const resolveOwner = (ownerJid) => {
|
|
295
|
+
const normalized = normalizeOwnerJid(ownerJid);
|
|
296
|
+
ensureValue(normalized, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'owner_jid inválido para operação do pack.');
|
|
297
|
+
return normalized;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const resolveOwnedPack = async (ownerJid, identifier, { connection = null } = {}) => {
|
|
301
|
+
const normalizedOwner = resolveOwner(ownerJid);
|
|
302
|
+
const candidates = buildIdentifierCandidates(identifier);
|
|
303
|
+
|
|
304
|
+
ensureValue(candidates.length > 0, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Informe o pack para continuar.');
|
|
305
|
+
|
|
306
|
+
for (const candidate of candidates) {
|
|
307
|
+
const pack = await deps.packRepository.findStickerPackByOwnerAndIdentifier(normalizedOwner, candidate, {
|
|
308
|
+
connection,
|
|
309
|
+
});
|
|
310
|
+
if (pack) return pack;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
throw buildError(STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND, 'Pack não encontrado para este usuário.');
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const resolvePackForSend = async (ownerJid, identifier, { connection = null } = {}) => {
|
|
317
|
+
const normalizedOwner = resolveOwner(ownerJid);
|
|
318
|
+
const candidates = buildIdentifierCandidates(identifier);
|
|
319
|
+
|
|
320
|
+
ensureValue(candidates.length > 0, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Informe o pack para continuar.');
|
|
321
|
+
|
|
322
|
+
for (const candidate of candidates) {
|
|
323
|
+
const owned = await deps.packRepository.findStickerPackByOwnerAndIdentifier(normalizedOwner, candidate, {
|
|
324
|
+
connection,
|
|
325
|
+
});
|
|
326
|
+
if (owned) return owned;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const candidate of candidates) {
|
|
330
|
+
const shared = await deps.packRepository.findStickerPackByPackKey(candidate, {
|
|
331
|
+
includeDeleted: false,
|
|
332
|
+
connection,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (shared && isShareableVisibility(shared.visibility)) {
|
|
336
|
+
return shared;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
throw buildError(STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND, 'Pack não encontrado para este usuário.');
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const loadPackDetails = async (pack, { connection = null } = {}) => {
|
|
344
|
+
const items = await deps.itemRepository.listStickerPackItems(pack.id, connection);
|
|
345
|
+
const coverItem = items.find((item) => item.sticker_id === pack.cover_sticker_id);
|
|
346
|
+
const packClassification = await getPackClassificationSummaryByAssetIds(items.map((item) => item.sticker_id)).catch(
|
|
347
|
+
() => null,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
...pack,
|
|
352
|
+
items,
|
|
353
|
+
cover_asset: coverItem?.asset || items[0]?.asset || null,
|
|
354
|
+
sticker_count: items.length,
|
|
355
|
+
classification: packClassification,
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const sanitizeMetadata = ({ name, publisher, description, visibility }) => {
|
|
360
|
+
const normalizedName = sanitizeText(name, MAX_NAME_LENGTH, { allowEmpty: false });
|
|
361
|
+
const normalizedPublisher = sanitizeText(publisher, MAX_PUBLISHER_LENGTH, { allowEmpty: false });
|
|
362
|
+
const normalizedDescription = sanitizeText(description, MAX_DESCRIPTION_LENGTH, { allowEmpty: true });
|
|
363
|
+
const normalizedVisibility = toVisibility(visibility, 'public');
|
|
364
|
+
|
|
365
|
+
ensureValue(normalizedName, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Nome do pack é obrigatório.');
|
|
366
|
+
ensureValue(normalizedPublisher, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Publisher do pack é obrigatório.');
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
name: normalizedName,
|
|
370
|
+
publisher: normalizedPublisher,
|
|
371
|
+
description: normalizedDescription || null,
|
|
372
|
+
visibility: normalizedVisibility,
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const createPack = async ({
|
|
377
|
+
ownerJid,
|
|
378
|
+
name,
|
|
379
|
+
publisher,
|
|
380
|
+
description,
|
|
381
|
+
visibility = 'public',
|
|
382
|
+
status = 'published',
|
|
383
|
+
packStatus = undefined,
|
|
384
|
+
packThemeKey = undefined,
|
|
385
|
+
packVolume = undefined,
|
|
386
|
+
isAutoPack = undefined,
|
|
387
|
+
lastRebalancedAt = undefined,
|
|
388
|
+
}) => {
|
|
389
|
+
const owner = resolveOwner(ownerJid);
|
|
390
|
+
const normalizedStatus = PACK_STATUS_VALUES.has(String(status || '').toLowerCase())
|
|
391
|
+
? String(status).toLowerCase()
|
|
392
|
+
: 'published';
|
|
393
|
+
|
|
394
|
+
return runAction('create_pack', { owner_jid: owner }, async () => {
|
|
395
|
+
return runInTransaction(async (connection) => {
|
|
396
|
+
const metadata = sanitizeMetadata({ name, publisher, description, visibility });
|
|
397
|
+
if (hasOwnerPackLimit) {
|
|
398
|
+
const existing = await deps.packRepository.listStickerPacksByOwner(owner, {
|
|
399
|
+
limit: maxPacksPerOwner + 1,
|
|
400
|
+
connection,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
ensureValue(
|
|
404
|
+
existing.length < maxPacksPerOwner,
|
|
405
|
+
STICKER_PACK_ERROR_CODES.PACK_LIMIT_REACHED,
|
|
406
|
+
`Limite de packs atingido (${maxPacksPerOwner}).`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const packKey = await ensurePackKey(owner, metadata.name, connection);
|
|
411
|
+
|
|
412
|
+
const created = await deps.packRepository.createStickerPack(
|
|
413
|
+
{
|
|
414
|
+
id: randomUUID(),
|
|
415
|
+
owner_jid: owner,
|
|
416
|
+
name: metadata.name,
|
|
417
|
+
publisher: metadata.publisher,
|
|
418
|
+
description: metadata.description,
|
|
419
|
+
pack_key: packKey,
|
|
420
|
+
cover_sticker_id: null,
|
|
421
|
+
visibility: metadata.visibility,
|
|
422
|
+
status: normalizedStatus,
|
|
423
|
+
pack_status: packStatus,
|
|
424
|
+
pack_theme_key: packThemeKey,
|
|
425
|
+
pack_volume: packVolume,
|
|
426
|
+
is_auto_pack: isAutoPack,
|
|
427
|
+
last_rebalanced_at: lastRebalancedAt,
|
|
428
|
+
version: 1,
|
|
429
|
+
},
|
|
430
|
+
connection,
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
return loadPackDetails(created, { connection });
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const listPacks = async ({ ownerJid, limit = 50 }) => {
|
|
439
|
+
const owner = resolveOwner(ownerJid);
|
|
440
|
+
|
|
441
|
+
return runAction('list_packs', { owner_jid: owner }, async () => {
|
|
442
|
+
const safeLimit = Math.max(1, Math.min(100, Number(limit) || 50));
|
|
443
|
+
return deps.packRepository.listStickerPacksByOwner(owner, { limit: safeLimit });
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const getPackInfo = async ({ ownerJid, identifier }) => {
|
|
448
|
+
const owner = resolveOwner(ownerJid);
|
|
449
|
+
|
|
450
|
+
return runAction('pack_info', { owner_jid: owner }, async () => {
|
|
451
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
452
|
+
return loadPackDetails(pack);
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const getPackInfoForSend = async ({ ownerJid, identifier }) => {
|
|
457
|
+
const owner = resolveOwner(ownerJid);
|
|
458
|
+
|
|
459
|
+
return runAction('pack_send_info', { owner_jid: owner }, async () => {
|
|
460
|
+
const pack = await resolvePackForSend(owner, identifier);
|
|
461
|
+
return loadPackDetails(pack);
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const renamePack = async ({ ownerJid, identifier, name }) => {
|
|
466
|
+
const owner = resolveOwner(ownerJid);
|
|
467
|
+
const normalizedName = sanitizeText(name, MAX_NAME_LENGTH, { allowEmpty: false });
|
|
468
|
+
|
|
469
|
+
ensureValue(normalizedName, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Novo nome inválido.');
|
|
470
|
+
|
|
471
|
+
return runAction('rename_pack', { owner_jid: owner }, async () => {
|
|
472
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
473
|
+
const updated = await deps.packRepository.updateStickerPackFields(pack.id, { name: normalizedName });
|
|
474
|
+
return loadPackDetails(updated);
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const setPackPublisher = async ({ ownerJid, identifier, publisher }) => {
|
|
479
|
+
const owner = resolveOwner(ownerJid);
|
|
480
|
+
const normalizedPublisher = sanitizeText(publisher, MAX_PUBLISHER_LENGTH, { allowEmpty: false });
|
|
481
|
+
|
|
482
|
+
ensureValue(normalizedPublisher, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Publisher inválido.');
|
|
483
|
+
|
|
484
|
+
return runAction('set_publisher', { owner_jid: owner }, async () => {
|
|
485
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
486
|
+
const updated = await deps.packRepository.updateStickerPackFields(pack.id, {
|
|
487
|
+
publisher: normalizedPublisher,
|
|
488
|
+
});
|
|
489
|
+
return loadPackDetails(updated);
|
|
490
|
+
});
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const setPackDescription = async ({ ownerJid, identifier, description }) => {
|
|
494
|
+
const owner = resolveOwner(ownerJid);
|
|
495
|
+
const normalizedDescription = sanitizeText(description, MAX_DESCRIPTION_LENGTH, { allowEmpty: true });
|
|
496
|
+
|
|
497
|
+
return runAction('set_description', { owner_jid: owner }, async () => {
|
|
498
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
499
|
+
const updated = await deps.packRepository.updateStickerPackFields(pack.id, {
|
|
500
|
+
description: normalizedDescription || null,
|
|
501
|
+
});
|
|
502
|
+
return loadPackDetails(updated);
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const setPackVisibility = async ({ ownerJid, identifier, visibility }) => {
|
|
507
|
+
const owner = resolveOwner(ownerJid);
|
|
508
|
+
const normalizedVisibility = toVisibility(visibility, null);
|
|
509
|
+
|
|
510
|
+
ensureValue(
|
|
511
|
+
normalizedVisibility,
|
|
512
|
+
STICKER_PACK_ERROR_CODES.INVALID_INPUT,
|
|
513
|
+
'Visibilidade inválida. Use: private, public ou unlisted.',
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
return runAction('set_visibility', { owner_jid: owner }, async () => {
|
|
517
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
518
|
+
const updated = await deps.packRepository.updateStickerPackFields(pack.id, {
|
|
519
|
+
visibility: normalizedVisibility,
|
|
520
|
+
});
|
|
521
|
+
return loadPackDetails(updated);
|
|
522
|
+
});
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const setPackCover = async ({ ownerJid, identifier, stickerId }) => {
|
|
526
|
+
const owner = resolveOwner(ownerJid);
|
|
527
|
+
const normalizedStickerId = sanitizeText(stickerId, 36, { allowEmpty: false });
|
|
528
|
+
|
|
529
|
+
ensureValue(normalizedStickerId, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Sticker inválido para capa.');
|
|
530
|
+
|
|
531
|
+
return runAction('set_cover', { owner_jid: owner }, async () => {
|
|
532
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
533
|
+
const item = await deps.itemRepository.getStickerPackItemByStickerId(pack.id, normalizedStickerId);
|
|
534
|
+
|
|
535
|
+
ensureValue(item, STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND, 'Essa figurinha não está no pack.');
|
|
536
|
+
|
|
537
|
+
const updated = await deps.packRepository.updateStickerPackFields(pack.id, {
|
|
538
|
+
cover_sticker_id: item.sticker_id,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
return loadPackDetails(updated);
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const addStickerToPack = async ({
|
|
546
|
+
ownerJid,
|
|
547
|
+
identifier,
|
|
548
|
+
asset,
|
|
549
|
+
emojis = [],
|
|
550
|
+
accessibilityLabel = null,
|
|
551
|
+
expectedErrorCodes = [],
|
|
552
|
+
}) => {
|
|
553
|
+
const owner = resolveOwner(ownerJid);
|
|
554
|
+
|
|
555
|
+
ensureValue(asset?.id, STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND, 'Nenhuma figurinha válida foi encontrada.');
|
|
556
|
+
|
|
557
|
+
const normalizedLabel = sanitizeText(accessibilityLabel, MAX_ACCESSIBILITY_LABEL_LENGTH, {
|
|
558
|
+
allowEmpty: true,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
return runAction('add_sticker', { owner_jid: owner }, async () => {
|
|
562
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
563
|
+
|
|
564
|
+
return runInTransaction(async (connection) => {
|
|
565
|
+
const existingItem = await deps.itemRepository.getStickerPackItemByStickerId(pack.id, asset.id, connection);
|
|
566
|
+
ensureValue(
|
|
567
|
+
!existingItem,
|
|
568
|
+
STICKER_PACK_ERROR_CODES.DUPLICATE_STICKER,
|
|
569
|
+
'Essa figurinha já está no pack.',
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
const total = await deps.itemRepository.countStickerPackItems(pack.id, connection);
|
|
573
|
+
ensureValue(
|
|
574
|
+
total < maxStickersPerPack,
|
|
575
|
+
STICKER_PACK_ERROR_CODES.PACK_LIMIT_REACHED,
|
|
576
|
+
`O pack atingiu o limite de ${maxStickersPerPack} figurinhas.`,
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
const maxPosition = await deps.itemRepository.getMaxStickerPackPosition(pack.id, connection);
|
|
580
|
+
|
|
581
|
+
await deps.itemRepository.createStickerPackItem(
|
|
582
|
+
{
|
|
583
|
+
id: randomUUID(),
|
|
584
|
+
pack_id: pack.id,
|
|
585
|
+
sticker_id: asset.id,
|
|
586
|
+
position: maxPosition + 1,
|
|
587
|
+
emojis: parseEmojiList(emojis),
|
|
588
|
+
accessibility_label: normalizedLabel || null,
|
|
589
|
+
},
|
|
590
|
+
connection,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
if (!pack.cover_sticker_id) {
|
|
594
|
+
await deps.packRepository.updateStickerPackFields(
|
|
595
|
+
pack.id,
|
|
596
|
+
{
|
|
597
|
+
cover_sticker_id: asset.id,
|
|
598
|
+
},
|
|
599
|
+
connection,
|
|
600
|
+
);
|
|
601
|
+
} else {
|
|
602
|
+
await deps.packRepository.bumpStickerPackVersion(pack.id, connection);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const reloaded = await deps.packRepository.findStickerPackByOwnerAndIdentifier(owner, pack.id, {
|
|
606
|
+
connection,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
return loadPackDetails(reloaded, { connection });
|
|
610
|
+
});
|
|
611
|
+
}, { expectedErrorCodes });
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const removeStickerFromPack = async ({ ownerJid, identifier, selector }) => {
|
|
615
|
+
const owner = resolveOwner(ownerJid);
|
|
616
|
+
|
|
617
|
+
return runAction('remove_sticker', { owner_jid: owner }, async () => {
|
|
618
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
619
|
+
|
|
620
|
+
return runInTransaction(async (connection) => {
|
|
621
|
+
let item = null;
|
|
622
|
+
const asNumber = Number(selector);
|
|
623
|
+
|
|
624
|
+
if (Number.isInteger(asNumber) && asNumber > 0) {
|
|
625
|
+
item = await deps.itemRepository.getStickerPackItemByPosition(pack.id, asNumber, connection);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!item) {
|
|
629
|
+
const stickerId = sanitizeText(selector, 36, { allowEmpty: false });
|
|
630
|
+
ensureValue(stickerId, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Informe o índice ou ID da figurinha.');
|
|
631
|
+
item = await deps.itemRepository.getStickerPackItemByStickerId(pack.id, stickerId, connection);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
ensureValue(item, STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND, 'Figurinha não encontrada no pack.');
|
|
635
|
+
|
|
636
|
+
await deps.itemRepository.removeStickerPackItemByStickerId(pack.id, item.sticker_id, connection);
|
|
637
|
+
await deps.itemRepository.shiftStickerPackPositionsAfter(pack.id, item.position, connection);
|
|
638
|
+
|
|
639
|
+
if (pack.cover_sticker_id === item.sticker_id) {
|
|
640
|
+
const remaining = await deps.itemRepository.listStickerPackItems(pack.id, connection);
|
|
641
|
+
const nextCover = remaining[0]?.sticker_id || null;
|
|
642
|
+
|
|
643
|
+
await deps.packRepository.updateStickerPackFields(
|
|
644
|
+
pack.id,
|
|
645
|
+
{
|
|
646
|
+
cover_sticker_id: nextCover,
|
|
647
|
+
},
|
|
648
|
+
connection,
|
|
649
|
+
);
|
|
650
|
+
} else {
|
|
651
|
+
await deps.packRepository.bumpStickerPackVersion(pack.id, connection);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const reloaded = await deps.packRepository.findStickerPackByOwnerAndIdentifier(owner, pack.id, {
|
|
655
|
+
connection,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
removed: item,
|
|
660
|
+
pack: await loadPackDetails(reloaded, { connection }),
|
|
661
|
+
};
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const reorderPackItems = async ({ ownerJid, identifier, orderStickerIds }) => {
|
|
667
|
+
const owner = resolveOwner(ownerJid);
|
|
668
|
+
ensureValue(
|
|
669
|
+
Array.isArray(orderStickerIds) && orderStickerIds.length > 0,
|
|
670
|
+
STICKER_PACK_ERROR_CODES.INVALID_INPUT,
|
|
671
|
+
'Envie a nova ordem de figurinhas.',
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
return runAction('reorder_pack', { owner_jid: owner }, async () => {
|
|
675
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
676
|
+
|
|
677
|
+
return runInTransaction(async (connection) => {
|
|
678
|
+
const currentItems = await deps.itemRepository.listStickerPackItems(pack.id, connection);
|
|
679
|
+
ensureValue(currentItems.length > 1, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'O pack precisa de 2+ figurinhas para reordenar.');
|
|
680
|
+
|
|
681
|
+
const currentIds = currentItems.map((item) => item.sticker_id);
|
|
682
|
+
const currentSet = new Set(currentIds);
|
|
683
|
+
const seen = new Set();
|
|
684
|
+
const requestedIds = [];
|
|
685
|
+
|
|
686
|
+
for (const rawId of orderStickerIds) {
|
|
687
|
+
const id = sanitizeText(rawId, 36, { allowEmpty: false });
|
|
688
|
+
if (!id || !currentSet.has(id) || seen.has(id)) continue;
|
|
689
|
+
requestedIds.push(id);
|
|
690
|
+
seen.add(id);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
ensureValue(
|
|
694
|
+
requestedIds.length > 0,
|
|
695
|
+
STICKER_PACK_ERROR_CODES.INVALID_INPUT,
|
|
696
|
+
'A ordem enviada não corresponde às figurinhas do pack.',
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const finalOrder = [...requestedIds, ...currentIds.filter((id) => !seen.has(id))];
|
|
700
|
+
|
|
701
|
+
if (!areArraysEqual(currentIds, finalOrder)) {
|
|
702
|
+
await deps.itemRepository.bulkUpdateStickerPackPositions(pack.id, finalOrder, connection);
|
|
703
|
+
await deps.packRepository.bumpStickerPackVersion(pack.id, connection);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const reloaded = await deps.packRepository.findStickerPackByOwnerAndIdentifier(owner, pack.id, {
|
|
707
|
+
connection,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
return loadPackDetails(reloaded, { connection });
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const clonePack = async ({ ownerJid, identifier, newName }) => {
|
|
716
|
+
const owner = resolveOwner(ownerJid);
|
|
717
|
+
const normalizedName = sanitizeText(newName, MAX_NAME_LENGTH, { allowEmpty: false });
|
|
718
|
+
|
|
719
|
+
ensureValue(normalizedName, STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Novo nome do clone é obrigatório.');
|
|
720
|
+
|
|
721
|
+
return runAction('clone_pack', { owner_jid: owner }, async () => {
|
|
722
|
+
const source = await resolveOwnedPack(owner, identifier);
|
|
723
|
+
|
|
724
|
+
return runInTransaction(async (connection) => {
|
|
725
|
+
const sourceItems = await deps.itemRepository.listStickerPackItems(source.id, connection);
|
|
726
|
+
const packKey = await ensurePackKey(owner, normalizedName);
|
|
727
|
+
|
|
728
|
+
const created = await deps.packRepository.createStickerPack(
|
|
729
|
+
{
|
|
730
|
+
id: randomUUID(),
|
|
731
|
+
owner_jid: owner,
|
|
732
|
+
name: normalizedName,
|
|
733
|
+
publisher: source.publisher,
|
|
734
|
+
description: source.description,
|
|
735
|
+
pack_key: packKey,
|
|
736
|
+
cover_sticker_id: source.cover_sticker_id,
|
|
737
|
+
visibility: 'public',
|
|
738
|
+
version: 1,
|
|
739
|
+
},
|
|
740
|
+
connection,
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
for (const item of sourceItems) {
|
|
744
|
+
await deps.itemRepository.createStickerPackItem(
|
|
745
|
+
{
|
|
746
|
+
id: randomUUID(),
|
|
747
|
+
pack_id: created.id,
|
|
748
|
+
sticker_id: item.sticker_id,
|
|
749
|
+
position: item.position,
|
|
750
|
+
emojis: item.emojis || [],
|
|
751
|
+
accessibility_label: item.accessibility_label || null,
|
|
752
|
+
},
|
|
753
|
+
connection,
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return loadPackDetails(created, { connection });
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const deletePack = async ({ ownerJid, identifier }) => {
|
|
763
|
+
const owner = resolveOwner(ownerJid);
|
|
764
|
+
|
|
765
|
+
return runAction('delete_pack', { owner_jid: owner }, async () => {
|
|
766
|
+
const pack = await resolveOwnedPack(owner, identifier);
|
|
767
|
+
const deleted = await deps.packRepository.softDeleteStickerPack(pack.id);
|
|
768
|
+
return loadPackDetails(deleted);
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
createPack,
|
|
774
|
+
listPacks,
|
|
775
|
+
getPackInfo,
|
|
776
|
+
getPackInfoForSend,
|
|
777
|
+
renamePack,
|
|
778
|
+
setPackPublisher,
|
|
779
|
+
setPackDescription,
|
|
780
|
+
setPackVisibility,
|
|
781
|
+
setPackCover,
|
|
782
|
+
addStickerToPack,
|
|
783
|
+
removeStickerFromPack,
|
|
784
|
+
reorderPackItems,
|
|
785
|
+
clonePack,
|
|
786
|
+
deletePack,
|
|
787
|
+
};
|
|
788
|
+
}
|