@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,108 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ const normalizeEngagementRow = (row) => ({
4
+ pack_id: row?.pack_id || null,
5
+ open_count: Number(row?.open_count || 0),
6
+ like_count: Number(row?.like_count || 0),
7
+ dislike_count: Number(row?.dislike_count || 0),
8
+ score: Number(row?.like_count || 0) - Number(row?.dislike_count || 0),
9
+ updated_at: row?.updated_at || null,
10
+ });
11
+
12
+ const EMPTY_ENGAGEMENT = Object.freeze({
13
+ open_count: 0,
14
+ like_count: 0,
15
+ dislike_count: 0,
16
+ score: 0,
17
+ updated_at: null,
18
+ });
19
+
20
+ export const getEmptyStickerPackEngagement = () => ({ ...EMPTY_ENGAGEMENT });
21
+
22
+ export async function listStickerPackEngagementByPackIds(packIds, connection = null) {
23
+ const ids = Array.from(new Set((Array.isArray(packIds) ? packIds : []).filter(Boolean)));
24
+ if (!ids.length) return new Map();
25
+
26
+ const placeholders = ids.map(() => '?').join(', ');
27
+ const rows = await executeQuery(
28
+ `SELECT pack_id, open_count, like_count, dislike_count, updated_at
29
+ FROM ${TABLES.STICKER_PACK_ENGAGEMENT}
30
+ WHERE pack_id IN (${placeholders})`,
31
+ ids,
32
+ connection,
33
+ );
34
+
35
+ const byPackId = new Map();
36
+ rows.forEach((row) => {
37
+ const normalized = normalizeEngagementRow(row);
38
+ if (normalized.pack_id) byPackId.set(normalized.pack_id, normalized);
39
+ });
40
+ return byPackId;
41
+ }
42
+
43
+ export async function getStickerPackEngagementByPackId(packId, connection = null) {
44
+ if (!packId) return getEmptyStickerPackEngagement();
45
+ const rows = await executeQuery(
46
+ `SELECT pack_id, open_count, like_count, dislike_count, updated_at
47
+ FROM ${TABLES.STICKER_PACK_ENGAGEMENT}
48
+ WHERE pack_id = ?
49
+ LIMIT 1`,
50
+ [packId],
51
+ connection,
52
+ );
53
+
54
+ if (!rows?.[0]) return getEmptyStickerPackEngagement();
55
+ return normalizeEngagementRow(rows[0]);
56
+ }
57
+
58
+ export async function incrementStickerPackOpen(packId, connection = null) {
59
+ if (!packId) return getEmptyStickerPackEngagement();
60
+
61
+ await executeQuery(
62
+ `INSERT INTO ${TABLES.STICKER_PACK_ENGAGEMENT}
63
+ (pack_id, open_count, like_count, dislike_count, last_opened_at)
64
+ VALUES (?, 1, 0, 0, CURRENT_TIMESTAMP)
65
+ ON DUPLICATE KEY UPDATE
66
+ open_count = open_count + 1,
67
+ last_opened_at = CURRENT_TIMESTAMP,
68
+ updated_at = CURRENT_TIMESTAMP`,
69
+ [packId],
70
+ connection,
71
+ );
72
+
73
+ return getStickerPackEngagementByPackId(packId, connection);
74
+ }
75
+
76
+ export async function incrementStickerPackLike(packId, connection = null) {
77
+ if (!packId) return getEmptyStickerPackEngagement();
78
+
79
+ await executeQuery(
80
+ `INSERT INTO ${TABLES.STICKER_PACK_ENGAGEMENT}
81
+ (pack_id, open_count, like_count, dislike_count)
82
+ VALUES (?, 0, 1, 0)
83
+ ON DUPLICATE KEY UPDATE
84
+ like_count = like_count + 1,
85
+ updated_at = CURRENT_TIMESTAMP`,
86
+ [packId],
87
+ connection,
88
+ );
89
+
90
+ return getStickerPackEngagementByPackId(packId, connection);
91
+ }
92
+
93
+ export async function incrementStickerPackDislike(packId, connection = null) {
94
+ if (!packId) return getEmptyStickerPackEngagement();
95
+
96
+ await executeQuery(
97
+ `INSERT INTO ${TABLES.STICKER_PACK_ENGAGEMENT}
98
+ (pack_id, open_count, like_count, dislike_count)
99
+ VALUES (?, 0, 0, 1)
100
+ ON DUPLICATE KEY UPDATE
101
+ dislike_count = dislike_count + 1,
102
+ updated_at = CURRENT_TIMESTAMP`,
103
+ [packId],
104
+ connection,
105
+ );
106
+
107
+ return getStickerPackEngagementByPackId(packId, connection);
108
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Erro padrão para operações relacionadas a packs de figurinha.
3
+ */
4
+ export class StickerPackError extends Error {
5
+ /**
6
+ * @param {string} code Código semântico do erro.
7
+ * @param {string} message Mensagem amigável para logs/cliente.
8
+ * @param {unknown} [details=null] Objeto técnico opcional para diagnóstico.
9
+ */
10
+ constructor(code, message, details = null) {
11
+ super(message);
12
+ this.name = 'StickerPackError';
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Catálogo de códigos de erro usados pelo domínio de sticker pack.
20
+ */
21
+ export const STICKER_PACK_ERROR_CODES = {
22
+ INVALID_INPUT: 'INVALID_INPUT',
23
+ PACK_NOT_FOUND: 'PACK_NOT_FOUND',
24
+ NOT_ALLOWED: 'NOT_ALLOWED',
25
+ STICKER_NOT_FOUND: 'STICKER_NOT_FOUND',
26
+ PACK_LIMIT_REACHED: 'PACK_LIMIT_REACHED',
27
+ DUPLICATE_STICKER: 'DUPLICATE_STICKER',
28
+ STORAGE_ERROR: 'STORAGE_ERROR',
29
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
30
+ };
@@ -0,0 +1,110 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ const clamp = (value, fallback, min, max) => {
4
+ const numeric = Number(value);
5
+ if (!Number.isFinite(numeric)) return fallback;
6
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
7
+ };
8
+
9
+ const normalizeInteraction = (value) => {
10
+ const normalized = String(value || '').trim().toLowerCase();
11
+ if (['open', 'like', 'dislike'].includes(normalized)) return normalized;
12
+ return null;
13
+ };
14
+
15
+ const sanitizeKey = (value, maxLength = 120) => {
16
+ const normalized = String(value || '')
17
+ .trim()
18
+ .replace(/\s+/g, ' ')
19
+ .slice(0, maxLength);
20
+ return normalized || null;
21
+ };
22
+
23
+ export async function createStickerPackInteractionEvent(
24
+ { packId, interaction, actorKey = null, sessionKey = null, source = null },
25
+ connection = null,
26
+ ) {
27
+ const normalizedInteraction = normalizeInteraction(interaction);
28
+ if (!packId || !normalizedInteraction) return false;
29
+
30
+ await executeQuery(
31
+ `INSERT INTO ${TABLES.STICKER_PACK_INTERACTION_EVENT}
32
+ (pack_id, interaction, actor_key, session_key, source)
33
+ VALUES (?, ?, ?, ?, ?)`,
34
+ [packId, normalizedInteraction, sanitizeKey(actorKey), sanitizeKey(sessionKey), sanitizeKey(source, 32)],
35
+ connection,
36
+ );
37
+ return true;
38
+ }
39
+
40
+ export async function listStickerPackInteractionStatsByPackIds(
41
+ packIds,
42
+ { horizonHours = 24, baselineDays = 7 } = {},
43
+ connection = null,
44
+ ) {
45
+ const ids = Array.from(new Set((Array.isArray(packIds) ? packIds : []).filter(Boolean)));
46
+ if (!ids.length) return new Map();
47
+
48
+ const safeHorizonHours = clamp(horizonHours, 24, 1, 240);
49
+ const safeBaselineDays = clamp(baselineDays, 7, 2, 60);
50
+ const placeholders = ids.map(() => '?').join(', ');
51
+
52
+ const rows = await executeQuery(
53
+ `SELECT
54
+ pack_id,
55
+ SUM(CASE WHEN interaction = 'open' AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeHorizonHours} HOUR) THEN 1 ELSE 0 END) AS open_horizon,
56
+ SUM(CASE WHEN interaction = 'open' AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeBaselineDays} DAY) THEN 1 ELSE 0 END) AS open_baseline,
57
+ SUM(CASE WHEN interaction = 'like' AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeHorizonHours} HOUR) THEN 1 ELSE 0 END) AS like_horizon,
58
+ SUM(CASE WHEN interaction = 'like' AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeBaselineDays} DAY) THEN 1 ELSE 0 END) AS like_baseline,
59
+ SUM(CASE WHEN interaction = 'dislike' AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeHorizonHours} HOUR) THEN 1 ELSE 0 END) AS dislike_horizon,
60
+ SUM(CASE WHEN interaction = 'dislike' AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeBaselineDays} DAY) THEN 1 ELSE 0 END) AS dislike_baseline
61
+ FROM ${TABLES.STICKER_PACK_INTERACTION_EVENT}
62
+ WHERE pack_id IN (${placeholders})
63
+ GROUP BY pack_id`,
64
+ ids,
65
+ connection,
66
+ );
67
+
68
+ const byPackId = new Map();
69
+ for (const row of rows) {
70
+ byPackId.set(row.pack_id, {
71
+ pack_id: row.pack_id,
72
+ open_horizon: Number(row.open_horizon || 0),
73
+ open_baseline: Number(row.open_baseline || 0),
74
+ like_horizon: Number(row.like_horizon || 0),
75
+ like_baseline: Number(row.like_baseline || 0),
76
+ dislike_horizon: Number(row.dislike_horizon || 0),
77
+ dislike_baseline: Number(row.dislike_baseline || 0),
78
+ });
79
+ }
80
+ return byPackId;
81
+ }
82
+
83
+ export async function listViewerRecentPackIds(
84
+ viewerKey,
85
+ { days = 30, limit = 120 } = {},
86
+ connection = null,
87
+ ) {
88
+ const normalizedViewer = sanitizeKey(viewerKey);
89
+ if (!normalizedViewer) return [];
90
+
91
+ const safeDays = clamp(days, 30, 1, 180);
92
+ const safeLimit = clamp(limit, 120, 5, 500);
93
+ const rows = await executeQuery(
94
+ `SELECT pack_id, COUNT(*) AS interactions, MAX(created_at) AS last_interaction_at
95
+ FROM ${TABLES.STICKER_PACK_INTERACTION_EVENT}
96
+ WHERE actor_key = ?
97
+ AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeDays} DAY)
98
+ GROUP BY pack_id
99
+ ORDER BY interactions DESC, last_interaction_at DESC
100
+ LIMIT ${safeLimit}`,
101
+ [normalizedViewer],
102
+ connection,
103
+ );
104
+
105
+ return rows.map((row) => ({
106
+ pack_id: row.pack_id,
107
+ interactions: Number(row.interactions || 0),
108
+ last_interaction_at: row.last_interaction_at || null,
109
+ }));
110
+ }
@@ -0,0 +1,440 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ /**
4
+ * Faz parse resiliente de JSON vindo do banco.
5
+ *
6
+ * @param {unknown} value Valor bruto.
7
+ * @param {unknown} [fallback=null] Valor fallback em caso de erro.
8
+ * @returns {unknown} Valor convertido ou fallback.
9
+ */
10
+ const parseJson = (value, fallback = null) => {
11
+ if (value === null || value === undefined) return fallback;
12
+ if (typeof value === 'object') return value;
13
+ if (Buffer.isBuffer(value)) {
14
+ try {
15
+ return JSON.parse(value.toString('utf8'));
16
+ } catch {
17
+ return fallback;
18
+ }
19
+ }
20
+
21
+ if (typeof value === 'string') {
22
+ try {
23
+ return JSON.parse(value);
24
+ } catch {
25
+ return fallback;
26
+ }
27
+ }
28
+
29
+ return fallback;
30
+ };
31
+
32
+ /**
33
+ * Normaliza um item do pack com dados opcionais de asset.
34
+ *
35
+ * @param {Record<string, unknown>|null|undefined} row Linha crua retornada da query.
36
+ * @returns {object|null} Item normalizado.
37
+ */
38
+ const normalizeItemRow = (row) => {
39
+ if (!row) return null;
40
+
41
+ return {
42
+ id: row.id,
43
+ pack_id: row.pack_id,
44
+ sticker_id: row.sticker_id,
45
+ position: Number(row.position || 0),
46
+ emojis: parseJson(row.emojis, []),
47
+ accessibility_label: row.accessibility_label || null,
48
+ created_at: row.created_at,
49
+ asset: row.asset_id
50
+ ? {
51
+ id: row.asset_id,
52
+ owner_jid: row.asset_owner_jid,
53
+ sha256: row.asset_sha256,
54
+ mimetype: row.asset_mimetype,
55
+ is_animated: row.asset_is_animated === 1 || row.asset_is_animated === true,
56
+ width: row.asset_width !== null && row.asset_width !== undefined ? Number(row.asset_width) : null,
57
+ height: row.asset_height !== null && row.asset_height !== undefined ? Number(row.asset_height) : null,
58
+ size_bytes:
59
+ row.asset_size_bytes !== null && row.asset_size_bytes !== undefined ? Number(row.asset_size_bytes) : 0,
60
+ storage_path: row.asset_storage_path,
61
+ created_at: row.asset_created_at,
62
+ }
63
+ : null,
64
+ };
65
+ };
66
+
67
+ /**
68
+ * Lista itens de um pack em ordem de posição.
69
+ *
70
+ * @param {string} packId ID do pack.
71
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
72
+ * @returns {Promise<object[]>} Itens do pack.
73
+ */
74
+ export async function listStickerPackItems(packId, connection = null) {
75
+ const rows = await executeQuery(
76
+ `SELECT
77
+ i.*,
78
+ a.id AS asset_id,
79
+ a.owner_jid AS asset_owner_jid,
80
+ a.sha256 AS asset_sha256,
81
+ a.mimetype AS asset_mimetype,
82
+ a.is_animated AS asset_is_animated,
83
+ a.width AS asset_width,
84
+ a.height AS asset_height,
85
+ a.size_bytes AS asset_size_bytes,
86
+ a.storage_path AS asset_storage_path,
87
+ a.created_at AS asset_created_at
88
+ FROM ${TABLES.STICKER_PACK_ITEM} i
89
+ LEFT JOIN ${TABLES.STICKER_ASSET} a ON a.id = i.sticker_id
90
+ WHERE i.pack_id = ?
91
+ ORDER BY i.position ASC`,
92
+ [packId],
93
+ connection,
94
+ );
95
+
96
+ return rows.map((row) => normalizeItemRow(row));
97
+ }
98
+
99
+ /**
100
+ * Lista itens de múltiplos packs em uma única consulta.
101
+ *
102
+ * @param {string[]} packIds IDs dos packs.
103
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null]
104
+ * @returns {Promise<object[]>}
105
+ */
106
+ export async function listStickerPackItemsByPackIds(packIds, connection = null) {
107
+ if (!Array.isArray(packIds) || packIds.length === 0) return [];
108
+ const uniquePackIds = Array.from(new Set(packIds.filter(Boolean)));
109
+ if (!uniquePackIds.length) return [];
110
+
111
+ const placeholders = uniquePackIds.map(() => '?').join(', ');
112
+ const rows = await executeQuery(
113
+ `SELECT
114
+ i.*,
115
+ a.id AS asset_id,
116
+ a.owner_jid AS asset_owner_jid,
117
+ a.sha256 AS asset_sha256,
118
+ a.mimetype AS asset_mimetype,
119
+ a.is_animated AS asset_is_animated,
120
+ a.width AS asset_width,
121
+ a.height AS asset_height,
122
+ a.size_bytes AS asset_size_bytes,
123
+ a.storage_path AS asset_storage_path,
124
+ a.created_at AS asset_created_at
125
+ FROM ${TABLES.STICKER_PACK_ITEM} i
126
+ LEFT JOIN ${TABLES.STICKER_ASSET} a ON a.id = i.sticker_id
127
+ WHERE i.pack_id IN (${placeholders})
128
+ ORDER BY i.pack_id ASC, i.position ASC`,
129
+ uniquePackIds,
130
+ connection,
131
+ );
132
+
133
+ return rows.map((row) => normalizeItemRow(row));
134
+ }
135
+
136
+ /**
137
+ * Busca item do pack pelo sticker_id.
138
+ *
139
+ * @param {string} packId ID do pack.
140
+ * @param {string} stickerId ID do asset/sticker.
141
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
142
+ * @returns {Promise<object|null>} Item encontrado.
143
+ */
144
+ export async function getStickerPackItemByStickerId(packId, stickerId, connection = null) {
145
+ const rows = await executeQuery(
146
+ `SELECT i.* FROM ${TABLES.STICKER_PACK_ITEM} i
147
+ WHERE i.pack_id = ? AND i.sticker_id = ?
148
+ LIMIT 1`,
149
+ [packId, stickerId],
150
+ connection,
151
+ );
152
+
153
+ return normalizeItemRow(rows?.[0] || null);
154
+ }
155
+
156
+ /**
157
+ * Busca item pela posição ordinal no pack.
158
+ *
159
+ * @param {string} packId ID do pack.
160
+ * @param {number} position Posição do item.
161
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
162
+ * @returns {Promise<object|null>} Item encontrado.
163
+ */
164
+ export async function getStickerPackItemByPosition(packId, position, connection = null) {
165
+ const rows = await executeQuery(
166
+ `SELECT i.* FROM ${TABLES.STICKER_PACK_ITEM} i
167
+ WHERE i.pack_id = ? AND i.position = ?
168
+ LIMIT 1`,
169
+ [packId, position],
170
+ connection,
171
+ );
172
+
173
+ return normalizeItemRow(rows?.[0] || null);
174
+ }
175
+
176
+ /**
177
+ * Conta quantos itens existem em um pack.
178
+ *
179
+ * @param {string} packId ID do pack.
180
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
181
+ * @returns {Promise<number>} Total de itens.
182
+ */
183
+ export async function countStickerPackItems(packId, connection = null) {
184
+ const rows = await executeQuery(
185
+ `SELECT COUNT(*) AS total FROM ${TABLES.STICKER_PACK_ITEM} WHERE pack_id = ?`,
186
+ [packId],
187
+ connection,
188
+ );
189
+
190
+ return Number(rows?.[0]?.total || 0);
191
+ }
192
+
193
+ /**
194
+ * Conta quantas referências um sticker possui em todos os packs.
195
+ *
196
+ * @param {string} stickerId ID do sticker/asset.
197
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
198
+ * @returns {Promise<number>} Total de referências em packs.
199
+ */
200
+ export async function countStickerPackItemRefsByStickerId(stickerId, connection = null) {
201
+ const rows = await executeQuery(
202
+ `SELECT COUNT(*) AS total FROM ${TABLES.STICKER_PACK_ITEM} WHERE sticker_id = ?`,
203
+ [stickerId],
204
+ connection,
205
+ );
206
+
207
+ return Number(rows?.[0]?.total || 0);
208
+ }
209
+
210
+ /**
211
+ * Obtém a maior posição atualmente usada no pack.
212
+ *
213
+ * @param {string} packId ID do pack.
214
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
215
+ * @returns {Promise<number>} Maior posição encontrada.
216
+ */
217
+ export async function getMaxStickerPackPosition(packId, connection = null) {
218
+ const rows = await executeQuery(
219
+ `SELECT MAX(position) AS max_position FROM ${TABLES.STICKER_PACK_ITEM} WHERE pack_id = ?`,
220
+ [packId],
221
+ connection,
222
+ );
223
+
224
+ const maxValue = rows?.[0]?.max_position;
225
+ return maxValue !== null && maxValue !== undefined ? Number(maxValue) : 0;
226
+ }
227
+
228
+ /**
229
+ * Cria um novo item dentro de um pack.
230
+ *
231
+ * @param {object} item Dados do item.
232
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
233
+ * @returns {Promise<object|null>} Item criado.
234
+ */
235
+ export async function createStickerPackItem(item, connection = null) {
236
+ await executeQuery(
237
+ `INSERT INTO ${TABLES.STICKER_PACK_ITEM}
238
+ (id, pack_id, sticker_id, position, emojis, accessibility_label)
239
+ VALUES (?, ?, ?, ?, ?, ?)`,
240
+ [
241
+ item.id,
242
+ item.pack_id,
243
+ item.sticker_id,
244
+ item.position,
245
+ item.emojis ? JSON.stringify(item.emojis) : JSON.stringify([]),
246
+ item.accessibility_label ?? null,
247
+ ],
248
+ connection,
249
+ );
250
+
251
+ return getStickerPackItemByStickerId(item.pack_id, item.sticker_id, connection);
252
+ }
253
+
254
+ /**
255
+ * Atualiza metadados do item (emojis/acessibilidade).
256
+ *
257
+ * @param {string} packId ID do pack.
258
+ * @param {string} stickerId ID do sticker.
259
+ * @param {Record<string, unknown>} fields Campos alteráveis.
260
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
261
+ * @returns {Promise<object|null>} Item atualizado.
262
+ */
263
+ export async function updateStickerPackItemMetadata(packId, stickerId, fields, connection = null) {
264
+ const clauses = [];
265
+ const params = [];
266
+
267
+ if ('emojis' in fields) {
268
+ clauses.push('emojis = ?');
269
+ params.push(fields.emojis ? JSON.stringify(fields.emojis) : JSON.stringify([]));
270
+ }
271
+
272
+ if ('accessibility_label' in fields) {
273
+ clauses.push('accessibility_label = ?');
274
+ params.push(fields.accessibility_label ?? null);
275
+ }
276
+
277
+ if (!clauses.length) {
278
+ return getStickerPackItemByStickerId(packId, stickerId, connection);
279
+ }
280
+
281
+ await executeQuery(
282
+ `UPDATE ${TABLES.STICKER_PACK_ITEM}
283
+ SET ${clauses.join(', ')}
284
+ WHERE pack_id = ? AND sticker_id = ?`,
285
+ [...params, packId, stickerId],
286
+ connection,
287
+ );
288
+
289
+ return getStickerPackItemByStickerId(packId, stickerId, connection);
290
+ }
291
+
292
+ /**
293
+ * Remove item por sticker_id e retorna o item removido.
294
+ *
295
+ * @param {string} packId ID do pack.
296
+ * @param {string} stickerId ID do sticker.
297
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
298
+ * @returns {Promise<object|null>} Item removido.
299
+ */
300
+ export async function removeStickerPackItemByStickerId(packId, stickerId, connection = null) {
301
+ const item = await getStickerPackItemByStickerId(packId, stickerId, connection);
302
+ if (!item) return null;
303
+
304
+ await executeQuery(
305
+ `DELETE FROM ${TABLES.STICKER_PACK_ITEM}
306
+ WHERE pack_id = ? AND sticker_id = ?`,
307
+ [packId, stickerId],
308
+ connection,
309
+ );
310
+
311
+ return item;
312
+ }
313
+
314
+ /**
315
+ * Remove item por posição e retorna o item removido.
316
+ *
317
+ * @param {string} packId ID do pack.
318
+ * @param {number} position Posição do item.
319
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
320
+ * @returns {Promise<object|null>} Item removido.
321
+ */
322
+ export async function removeStickerPackItemByPosition(packId, position, connection = null) {
323
+ const item = await getStickerPackItemByPosition(packId, position, connection);
324
+ if (!item) return null;
325
+
326
+ await executeQuery(
327
+ `DELETE FROM ${TABLES.STICKER_PACK_ITEM}
328
+ WHERE pack_id = ? AND position = ?`,
329
+ [packId, position],
330
+ connection,
331
+ );
332
+
333
+ return item;
334
+ }
335
+
336
+ /**
337
+ * Remove todos os itens de um pack.
338
+ *
339
+ * @param {string} packId ID do pack.
340
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
341
+ * @returns {Promise<number>} Quantidade de linhas removidas.
342
+ */
343
+ export async function removeStickerPackItemsByPackId(packId, connection = null) {
344
+ const result = await executeQuery(
345
+ `DELETE FROM ${TABLES.STICKER_PACK_ITEM}
346
+ WHERE pack_id = ?`,
347
+ [packId],
348
+ connection,
349
+ );
350
+
351
+ return Number(result?.affectedRows || 0);
352
+ }
353
+
354
+ /**
355
+ * Reordena automaticamente os itens após remover uma posição.
356
+ *
357
+ * @param {string} packId ID do pack.
358
+ * @param {number} removedPosition Posição removida.
359
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
360
+ * @returns {Promise<void>}
361
+ */
362
+ export async function shiftStickerPackPositionsAfter(packId, removedPosition, connection = null) {
363
+ const SHIFT_OFFSET = 10000;
364
+
365
+ // Fase 1: move posições para uma faixa temporária, evitando colisão no índice único (pack_id, position).
366
+ await executeQuery(
367
+ `UPDATE ${TABLES.STICKER_PACK_ITEM}
368
+ SET position = position + ?
369
+ WHERE pack_id = ? AND position > ?`,
370
+ [SHIFT_OFFSET, packId, removedPosition],
371
+ connection,
372
+ );
373
+
374
+ // Fase 2: normaliza removendo o offset e deslocando 1 posição para preencher o "gap" do item removido.
375
+ await executeQuery(
376
+ `UPDATE ${TABLES.STICKER_PACK_ITEM}
377
+ SET position = position - ?
378
+ WHERE pack_id = ? AND position > ?`,
379
+ [SHIFT_OFFSET + 1, packId, removedPosition + SHIFT_OFFSET],
380
+ connection,
381
+ );
382
+ }
383
+
384
+ /**
385
+ * Aplica uma nova ordem explícita para os stickers do pack.
386
+ *
387
+ * @param {string} packId ID do pack.
388
+ * @param {string[]} orderedStickerIds IDs na ordem desejada.
389
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
390
+ * @returns {Promise<void>}
391
+ */
392
+ export async function bulkUpdateStickerPackPositions(packId, orderedStickerIds, connection = null) {
393
+ if (!Array.isArray(orderedStickerIds) || orderedStickerIds.length === 0) return;
394
+
395
+ await executeQuery(
396
+ `UPDATE ${TABLES.STICKER_PACK_ITEM}
397
+ SET position = position + 10000
398
+ WHERE pack_id = ?`,
399
+ [packId],
400
+ connection,
401
+ );
402
+
403
+ for (let index = 0; index < orderedStickerIds.length; index += 1) {
404
+ const stickerId = orderedStickerIds[index];
405
+ await executeQuery(
406
+ `UPDATE ${TABLES.STICKER_PACK_ITEM}
407
+ SET position = ?
408
+ WHERE pack_id = ? AND sticker_id = ?`,
409
+ [index + 1, packId, stickerId],
410
+ connection,
411
+ );
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Clona todos os itens de um pack para outro.
417
+ *
418
+ * @param {string} sourcePackId Pack de origem.
419
+ * @param {string} targetPackId Pack de destino.
420
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
421
+ * @returns {Promise<void>}
422
+ */
423
+ export async function cloneStickerPackItems(sourcePackId, targetPackId, connection = null) {
424
+ const items = await listStickerPackItems(sourcePackId, connection);
425
+ for (const item of items) {
426
+ await executeQuery(
427
+ `INSERT INTO ${TABLES.STICKER_PACK_ITEM}
428
+ (id, pack_id, sticker_id, position, emojis, accessibility_label)
429
+ VALUES (UUID(), ?, ?, ?, ?, ?)`,
430
+ [
431
+ targetPackId,
432
+ item.sticker_id,
433
+ item.position,
434
+ item.emojis ? JSON.stringify(item.emojis) : JSON.stringify([]),
435
+ item.accessibility_label ?? null,
436
+ ],
437
+ connection,
438
+ );
439
+ }
440
+ }