@kaikybrofc/omnizap-system 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. package/scripts/release.sh +129 -0
@@ -0,0 +1,442 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ const CATALOG_COMPLETE_PACK_TARGET = Math.max(1, Number(process.env.STICKER_PACK_MAX_ITEMS) || 30);
4
+
5
+ /**
6
+ * Normaliza linha da tabela de packs para formato usado no domínio.
7
+ *
8
+ * @param {Record<string, unknown>|null|undefined} row Linha retornada da query.
9
+ * @returns {object|null} Pack normalizado.
10
+ */
11
+ const normalizeStickerPackRow = (row) => {
12
+ if (!row) return null;
13
+
14
+ return {
15
+ id: row.id,
16
+ owner_jid: row.owner_jid,
17
+ name: row.name,
18
+ publisher: row.publisher,
19
+ description: row.description,
20
+ pack_key: row.pack_key,
21
+ cover_sticker_id: row.cover_sticker_id,
22
+ visibility: row.visibility,
23
+ status: row.status || 'published',
24
+ pack_status: row.pack_status || 'ready',
25
+ pack_theme_key: row.pack_theme_key || null,
26
+ pack_volume: row.pack_volume !== null && row.pack_volume !== undefined ? Number(row.pack_volume) : null,
27
+ is_auto_pack: row.is_auto_pack === 1 || row.is_auto_pack === true,
28
+ last_rebalanced_at: row.last_rebalanced_at || null,
29
+ version: Number(row.version || 1),
30
+ created_at: row.created_at,
31
+ updated_at: row.updated_at,
32
+ deleted_at: row.deleted_at,
33
+ sticker_count: row.sticker_count !== null && row.sticker_count !== undefined ? Number(row.sticker_count) : undefined,
34
+ };
35
+ };
36
+
37
+ /**
38
+ * Busca pack por ID.
39
+ *
40
+ * @param {string} packId ID interno do pack.
41
+ * @param {{ includeDeleted?: boolean, connection?: import('mysql2/promise').PoolConnection|null }} [options]
42
+ * @returns {Promise<object|null>} Pack encontrado.
43
+ */
44
+ export async function findStickerPackById(packId, { includeDeleted = false, connection = null } = {}) {
45
+ const rows = await executeQuery(
46
+ `SELECT p.*,
47
+ (SELECT COUNT(*) FROM ${TABLES.STICKER_PACK_ITEM} i WHERE i.pack_id = p.id) AS sticker_count
48
+ FROM ${TABLES.STICKER_PACK} p
49
+ WHERE p.id = ? ${includeDeleted ? '' : 'AND p.deleted_at IS NULL'}
50
+ LIMIT 1`,
51
+ [packId],
52
+ connection,
53
+ );
54
+
55
+ return normalizeStickerPackRow(rows?.[0] || null);
56
+ }
57
+
58
+ /**
59
+ * Busca pack por chave pública (pack_key).
60
+ *
61
+ * @param {string} packKey Chave pública do pack.
62
+ * @param {{ includeDeleted?: boolean, connection?: import('mysql2/promise').PoolConnection|null }} [options]
63
+ * @returns {Promise<object|null>} Pack encontrado.
64
+ */
65
+ export async function findStickerPackByPackKey(packKey, { includeDeleted = false, connection = null } = {}) {
66
+ const rows = await executeQuery(
67
+ `SELECT p.*,
68
+ (SELECT COUNT(*) FROM ${TABLES.STICKER_PACK_ITEM} i WHERE i.pack_id = p.id) AS sticker_count
69
+ FROM ${TABLES.STICKER_PACK} p
70
+ WHERE p.pack_key = ? ${includeDeleted ? '' : 'AND p.deleted_at IS NULL'}
71
+ LIMIT 1`,
72
+ [packKey],
73
+ connection,
74
+ );
75
+
76
+ return normalizeStickerPackRow(rows?.[0] || null);
77
+ }
78
+
79
+ /**
80
+ * Busca pack do dono por ID, pack_key ou nome.
81
+ *
82
+ * @param {string} ownerJid JID do dono.
83
+ * @param {string} identifier ID, chave ou nome do pack.
84
+ * @param {{ includeDeleted?: boolean, connection?: import('mysql2/promise').PoolConnection|null }} [options]
85
+ * @returns {Promise<object|null>} Pack encontrado.
86
+ */
87
+ export async function findStickerPackByOwnerAndIdentifier(
88
+ ownerJid,
89
+ identifier,
90
+ { includeDeleted = false, connection = null } = {},
91
+ ) {
92
+ if (!identifier) return null;
93
+
94
+ const idOrPack = await executeQuery(
95
+ `SELECT p.*,
96
+ (SELECT COUNT(*) FROM ${TABLES.STICKER_PACK_ITEM} i WHERE i.pack_id = p.id) AS sticker_count
97
+ FROM ${TABLES.STICKER_PACK} p
98
+ WHERE p.owner_jid = ?
99
+ ${includeDeleted ? '' : 'AND p.deleted_at IS NULL'}
100
+ AND (p.id = ? OR p.pack_key = ?)
101
+ ORDER BY p.updated_at DESC
102
+ LIMIT 1`,
103
+ [ownerJid, identifier, identifier],
104
+ connection,
105
+ );
106
+
107
+ if (idOrPack?.[0]) {
108
+ return normalizeStickerPackRow(idOrPack[0]);
109
+ }
110
+
111
+ const byName = await executeQuery(
112
+ `SELECT p.*,
113
+ (SELECT COUNT(*) FROM ${TABLES.STICKER_PACK_ITEM} i WHERE i.pack_id = p.id) AS sticker_count
114
+ FROM ${TABLES.STICKER_PACK} p
115
+ WHERE p.owner_jid = ?
116
+ ${includeDeleted ? '' : 'AND p.deleted_at IS NULL'}
117
+ AND LOWER(p.name) = LOWER(?)
118
+ ORDER BY p.updated_at DESC
119
+ LIMIT 1`,
120
+ [ownerJid, identifier],
121
+ connection,
122
+ );
123
+
124
+ return normalizeStickerPackRow(byName?.[0] || null);
125
+ }
126
+
127
+ /**
128
+ * Lista packs de um usuário com paginação simples.
129
+ *
130
+ * @param {string} ownerJid JID do dono.
131
+ * @param {{ includeDeleted?: boolean, limit?: number, offset?: number, connection?: import('mysql2/promise').PoolConnection|null }} [options]
132
+ * @returns {Promise<object[]>} Lista de packs.
133
+ */
134
+ export async function listStickerPacksByOwner(
135
+ ownerJid,
136
+ { includeDeleted = false, limit = 50, offset = 0, connection = null } = {},
137
+ ) {
138
+ const safeLimit = Math.max(1, Number(limit) || 50);
139
+ const safeOffset = Math.max(0, Number(offset) || 0);
140
+
141
+ const rows = await executeQuery(
142
+ `SELECT p.*,
143
+ (SELECT COUNT(*) FROM ${TABLES.STICKER_PACK_ITEM} i WHERE i.pack_id = p.id) AS sticker_count
144
+ FROM ${TABLES.STICKER_PACK} p
145
+ WHERE p.owner_jid = ? ${includeDeleted ? '' : 'AND p.deleted_at IS NULL'}
146
+ ORDER BY p.updated_at DESC
147
+ LIMIT ${safeLimit} OFFSET ${safeOffset}`,
148
+ [ownerJid],
149
+ connection,
150
+ );
151
+
152
+ return rows.map((row) => normalizeStickerPackRow(row));
153
+ }
154
+
155
+ /**
156
+ * Lista packs automáticos de curadoria para um conjunto de owners.
157
+ *
158
+ * @param {{
159
+ * ownerJids?: string[],
160
+ * includeArchived?: boolean,
161
+ * themeKey?: string,
162
+ * limit?: number,
163
+ * offset?: number,
164
+ * connection?: import('mysql2/promise').PoolConnection|null,
165
+ * }} [options]
166
+ * @returns {Promise<object[]>}
167
+ */
168
+ export async function listStickerAutoPacksForCuration({
169
+ ownerJids = [],
170
+ includeArchived = true,
171
+ themeKey = '',
172
+ includeLegacyMarkers = true,
173
+ limit = 2000,
174
+ offset = 0,
175
+ connection = null,
176
+ } = {}) {
177
+ const owners = Array.from(new Set((Array.isArray(ownerJids) ? ownerJids : []).filter(Boolean)));
178
+ if (!owners.length) return [];
179
+
180
+ const safeLimit = Math.max(1, Math.min(5000, Number(limit) || 2000));
181
+ const safeOffset = Math.max(0, Number(offset) || 0);
182
+ const ownerPlaceholders = owners.map(() => '?').join(', ');
183
+
184
+ const whereClauses = [
185
+ 'p.deleted_at IS NULL',
186
+ `p.owner_jid IN (${ownerPlaceholders})`,
187
+ ];
188
+ const params = [...owners];
189
+
190
+ if (includeLegacyMarkers) {
191
+ whereClauses.push("(p.is_auto_pack = 1 OR p.description LIKE '%[auto-theme:%' OR p.description LIKE '%[auto-tag:%')");
192
+ } else {
193
+ whereClauses.push('p.is_auto_pack = 1');
194
+ }
195
+
196
+ const normalizedThemeKey = String(themeKey || '').trim().toLowerCase();
197
+ if (normalizedThemeKey) {
198
+ whereClauses.push('LOWER(COALESCE(p.pack_theme_key, \'\')) = ?');
199
+ params.push(normalizedThemeKey);
200
+ }
201
+
202
+ if (!includeArchived) {
203
+ whereClauses.push("COALESCE(p.pack_status, 'ready') <> 'archived'");
204
+ }
205
+
206
+ const rows = await executeQuery(
207
+ `SELECT p.*,
208
+ (SELECT COUNT(*) FROM ${TABLES.STICKER_PACK_ITEM} i WHERE i.pack_id = p.id) AS sticker_count
209
+ FROM ${TABLES.STICKER_PACK} p
210
+ WHERE ${whereClauses.join(' AND ')}
211
+ ORDER BY
212
+ COALESCE(p.pack_theme_key, '') ASC,
213
+ COALESCE(p.pack_volume, 0) ASC,
214
+ p.updated_at DESC
215
+ LIMIT ${safeLimit} OFFSET ${safeOffset}`,
216
+ params,
217
+ connection,
218
+ );
219
+
220
+ return rows.map((row) => normalizeStickerPackRow(row));
221
+ }
222
+
223
+ /**
224
+ * Lista packs públicos para catálogo web com busca e paginação.
225
+ *
226
+ * @param {{
227
+ * visibility?: 'public'|'unlisted'|'all',
228
+ * search?: string,
229
+ * limit?: number,
230
+ * offset?: number,
231
+ * connection?: import('mysql2/promise').PoolConnection|null,
232
+ * }} [options] Filtros de listagem.
233
+ * @returns {Promise<{ packs: object[], hasMore: boolean }>} Resultado paginado.
234
+ */
235
+ export async function listStickerPacksForCatalog({
236
+ visibility = 'public',
237
+ search = '',
238
+ limit = 24,
239
+ offset = 0,
240
+ connection = null,
241
+ } = {}) {
242
+ const safeLimit = Math.max(1, Math.min(60, Number(limit) || 24));
243
+ const safeOffset = Math.max(0, Number(offset) || 0);
244
+ const safeLimitWithSentinel = safeLimit + 1;
245
+
246
+ const normalizedVisibility = String(visibility || 'public').trim().toLowerCase();
247
+ const visibilityValues =
248
+ normalizedVisibility === 'all'
249
+ ? ['public', 'unlisted']
250
+ : normalizedVisibility === 'unlisted'
251
+ ? ['unlisted']
252
+ : ['public'];
253
+
254
+ const normalizedSearch = String(search || '').trim().toLowerCase().slice(0, 120);
255
+ const params = [...visibilityValues];
256
+ const whereClauses = [
257
+ 'p.deleted_at IS NULL',
258
+ "p.status = 'published'",
259
+ "COALESCE(p.pack_status, 'ready') = 'ready'",
260
+ `p.visibility IN (${visibilityValues.map(() => '?').join(', ')})`,
261
+ ];
262
+
263
+ if (normalizedSearch) {
264
+ const like = `%${normalizedSearch}%`;
265
+ whereClauses.push(
266
+ '(LOWER(p.name) LIKE ? OR LOWER(p.publisher) LIKE ? OR LOWER(COALESCE(p.description, \'\')) LIKE ? OR LOWER(p.pack_key) LIKE ?)',
267
+ );
268
+ params.push(like, like, like, like);
269
+ }
270
+
271
+ const rows = await executeQuery(
272
+ `SELECT p.*,
273
+ (SELECT COUNT(*) FROM ${TABLES.STICKER_PACK_ITEM} i WHERE i.pack_id = p.id) AS sticker_count
274
+ FROM ${TABLES.STICKER_PACK} p
275
+ WHERE ${whereClauses.join(' AND ')}
276
+ ORDER BY
277
+ (sticker_count >= ${CATALOG_COMPLETE_PACK_TARGET}) DESC,
278
+ sticker_count DESC,
279
+ p.updated_at DESC
280
+ LIMIT ${safeLimitWithSentinel} OFFSET ${safeOffset}`,
281
+ params,
282
+ connection,
283
+ );
284
+
285
+ const hasMore = rows.length > safeLimit;
286
+ return {
287
+ packs: rows.slice(0, safeLimit).map((row) => normalizeStickerPackRow(row)),
288
+ hasMore,
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Cria um registro de pack.
294
+ *
295
+ * @param {object} pack Dados do pack.
296
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
297
+ * @returns {Promise<object|null>} Pack criado.
298
+ */
299
+ export async function createStickerPack(pack, connection = null) {
300
+ await executeQuery(
301
+ `INSERT INTO ${TABLES.STICKER_PACK}
302
+ (
303
+ id,
304
+ owner_jid,
305
+ name,
306
+ publisher,
307
+ description,
308
+ pack_key,
309
+ cover_sticker_id,
310
+ visibility,
311
+ status,
312
+ pack_status,
313
+ pack_theme_key,
314
+ pack_volume,
315
+ is_auto_pack,
316
+ last_rebalanced_at,
317
+ version
318
+ )
319
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
320
+ [
321
+ pack.id,
322
+ pack.owner_jid,
323
+ pack.name,
324
+ pack.publisher,
325
+ pack.description ?? null,
326
+ pack.pack_key,
327
+ pack.cover_sticker_id ?? null,
328
+ pack.visibility,
329
+ pack.status ?? 'published',
330
+ pack.pack_status ?? 'ready',
331
+ pack.pack_theme_key ?? null,
332
+ pack.pack_volume ?? null,
333
+ pack.is_auto_pack ? 1 : 0,
334
+ pack.last_rebalanced_at ?? null,
335
+ pack.version ?? 1,
336
+ ],
337
+ connection,
338
+ );
339
+
340
+ return findStickerPackById(pack.id, { includeDeleted: true, connection });
341
+ }
342
+
343
+ const UPDATE_FIELD_MAP = {
344
+ name: 'name',
345
+ publisher: 'publisher',
346
+ description: 'description',
347
+ pack_key: 'pack_key',
348
+ cover_sticker_id: 'cover_sticker_id',
349
+ visibility: 'visibility',
350
+ status: 'status',
351
+ pack_status: 'pack_status',
352
+ pack_theme_key: 'pack_theme_key',
353
+ pack_volume: 'pack_volume',
354
+ is_auto_pack: 'is_auto_pack',
355
+ last_rebalanced_at: 'last_rebalanced_at',
356
+ deleted_at: 'deleted_at',
357
+ };
358
+
359
+ /**
360
+ * Atualiza campos permitidos de um pack.
361
+ *
362
+ * @param {string} packId ID do pack.
363
+ * @param {Record<string, unknown>} fields Campos para atualização.
364
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
365
+ * @returns {Promise<object|null>} Pack atualizado.
366
+ */
367
+ export async function updateStickerPackFields(packId, fields, connection = null) {
368
+ const setClauses = [];
369
+ const params = [];
370
+
371
+ for (const [field, column] of Object.entries(UPDATE_FIELD_MAP)) {
372
+ if (!(field in fields)) continue;
373
+ setClauses.push(`${column} = ?`);
374
+ params.push(fields[field]);
375
+ }
376
+
377
+ if (!setClauses.length) {
378
+ return findStickerPackById(packId, { includeDeleted: true, connection });
379
+ }
380
+
381
+ setClauses.push('version = version + 1');
382
+ setClauses.push('updated_at = CURRENT_TIMESTAMP');
383
+
384
+ await executeQuery(
385
+ `UPDATE ${TABLES.STICKER_PACK}
386
+ SET ${setClauses.join(', ')}
387
+ WHERE id = ?`,
388
+ [...params, packId],
389
+ connection,
390
+ );
391
+
392
+ return findStickerPackById(packId, { includeDeleted: true, connection });
393
+ }
394
+
395
+ /**
396
+ * Marca um pack como deletado sem remover dados físicos.
397
+ *
398
+ * @param {string} packId ID do pack.
399
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
400
+ * @returns {Promise<object|null>} Pack atualizado.
401
+ */
402
+ export async function softDeleteStickerPack(packId, connection = null) {
403
+ return updateStickerPackFields(
404
+ packId,
405
+ {
406
+ deleted_at: new Date(),
407
+ },
408
+ connection,
409
+ );
410
+ }
411
+
412
+ /**
413
+ * Verifica se a chave pública (pack_key) está disponível.
414
+ *
415
+ * @param {string} packKey Chave candidata.
416
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
417
+ * @returns {Promise<boolean>} `true` quando não existe pack com essa chave.
418
+ */
419
+ export async function ensureUniquePackKey(packKey, connection = null) {
420
+ const existing = await findStickerPackByPackKey(packKey, { includeDeleted: true, connection });
421
+ return !existing;
422
+ }
423
+
424
+ /**
425
+ * Incrementa versão e timestamp de atualização do pack.
426
+ *
427
+ * @param {string} packId ID do pack.
428
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
429
+ * @returns {Promise<object|null>} Pack atualizado.
430
+ */
431
+ export async function bumpStickerPackVersion(packId, connection = null) {
432
+ await executeQuery(
433
+ `UPDATE ${TABLES.STICKER_PACK}
434
+ SET version = version + 1,
435
+ updated_at = CURRENT_TIMESTAMP
436
+ WHERE id = ?`,
437
+ [packId],
438
+ connection,
439
+ );
440
+
441
+ return findStickerPackById(packId, { includeDeleted: true, connection });
442
+ }