@kaikybrofc/omnizap-system 2.2.4 → 2.2.5

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 (39) hide show
  1. package/.env.example +5 -0
  2. package/README.md +13 -13
  3. package/app/modules/stickerPackModule/catalogHandlers/catalogAdminHttp.js +68 -0
  4. package/app/modules/stickerPackModule/catalogHandlers/catalogAuthHttp.js +34 -0
  5. package/app/modules/stickerPackModule/catalogHandlers/catalogPublicHttp.js +179 -0
  6. package/app/modules/stickerPackModule/catalogHandlers/catalogUploadHttp.js +92 -0
  7. package/app/modules/stickerPackModule/catalogRouter.js +79 -0
  8. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +243 -0
  9. package/app/modules/stickerPackModule/domainEvents.js +61 -0
  10. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +21 -0
  11. package/app/modules/stickerPackModule/stickerAssetRepository.js +19 -0
  12. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +55 -15
  13. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +238 -0
  14. package/app/modules/stickerPackModule/stickerDomainEventBus.js +71 -0
  15. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +198 -0
  16. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  17. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +537 -529
  18. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +44 -0
  19. package/app/modules/stickerPackModule/stickerPackItemRepository.js +18 -0
  20. package/app/modules/stickerPackModule/stickerPackRepository.js +51 -0
  21. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +191 -0
  22. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +301 -0
  23. package/app/modules/stickerPackModule/stickerStorageService.js +111 -10
  24. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +21 -0
  25. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +59 -7
  26. package/app/observability/metrics.js +169 -0
  27. package/app/services/featureFlagService.js +137 -0
  28. package/database/index.js +5 -0
  29. package/database/migrations/20260228_0022_sticker_scale_indexes.sql +16 -0
  30. package/database/migrations/20260228_0023_sticker_pack_score_snapshot.sql +25 -0
  31. package/database/migrations/20260228_0024_domain_event_outbox.sql +42 -0
  32. package/database/migrations/20260228_0025_sticker_worker_task_idempotency_dlq.sql +23 -0
  33. package/database/migrations/20260228_0026_feature_flags.sql +21 -0
  34. package/ecosystem.prod.config.cjs +70 -9
  35. package/index.js +26 -0
  36. package/package.json +5 -1
  37. package/public/index.html +30 -3
  38. package/scripts/sticker-catalog-loadtest.mjs +208 -0
  39. package/scripts/sticker-worker-task.mjs +122 -0
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
4
+ import { URL, URLSearchParams } from 'node:url';
4
5
  import axios from 'axios';
5
6
 
6
7
  import { executeQuery, pool, TABLES } from '../../../database/index.js';
@@ -64,6 +65,8 @@ import {
64
65
  buildViewerTagAffinity,
65
66
  computePackSignals,
66
67
  } from './stickerPackMarketplaceService.js';
68
+ import { listStickerPackScoreSnapshotsByPackIds } from './stickerPackScoreSnapshotRepository.js';
69
+ import { createCatalogApiRouter } from './catalogRouter.js';
67
70
  import {
68
71
  buildAdminMenu,
69
72
  buildAiMenu,
@@ -75,11 +78,17 @@ import {
75
78
  buildStickerMenu,
76
79
  } from '../menuModule/common.js';
77
80
  import { getMarketplaceDriftSnapshot } from './stickerMarketplaceDriftService.js';
78
- import { getStickerStorageConfig, readStickerAssetBuffer, saveStickerAssetFromBuffer } from './stickerStorageService.js';
81
+ import {
82
+ getStickerAssetExternalUrl,
83
+ getStickerStorageConfig,
84
+ readStickerAssetBuffer,
85
+ saveStickerAssetFromBuffer,
86
+ } from './stickerStorageService.js';
79
87
  import { convertToWebp } from '../stickerModule/convertToWebp.js';
80
88
  import { sanitizeText } from './stickerPackUtils.js';
81
89
  import stickerPackService from './stickerPackServiceRuntime.js';
82
90
  import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
91
+ import { isFeatureEnabled } from '../../services/featureFlagService.js';
83
92
 
84
93
  const parseEnvBool = (value, fallback) => {
85
94
  if (value === undefined || value === null || value === '') return fallback;
@@ -203,6 +212,19 @@ const GITHUB_REPOSITORY = String(process.env.GITHUB_REPOSITORY || 'Kaikygr/omniz
203
212
  const GITHUB_TOKEN = String(process.env.GITHUB_TOKEN || '').trim();
204
213
  const GITHUB_PROJECT_CACHE_SECONDS = clampInt(process.env.GITHUB_PROJECT_CACHE_SECONDS, 300, 30, 3600);
205
214
  const GLOBAL_RANK_REFRESH_SECONDS = clampInt(process.env.GLOBAL_RANK_REFRESH_SECONDS, 600, 60, 3600);
215
+ const CATALOG_LIST_CACHE_SECONDS = clampInt(process.env.STICKER_CATALOG_LIST_CACHE_SECONDS, 90, 15, 900);
216
+ const CATALOG_CREATOR_RANKING_CACHE_SECONDS = clampInt(
217
+ process.env.STICKER_CATALOG_CREATOR_RANKING_CACHE_SECONDS,
218
+ 120,
219
+ 15,
220
+ 900,
221
+ );
222
+ const CATALOG_PACK_PAYLOAD_CACHE_SECONDS = clampInt(
223
+ process.env.STICKER_CATALOG_PACK_PAYLOAD_CACHE_SECONDS,
224
+ 300,
225
+ 30,
226
+ 1800,
227
+ );
206
228
  const MARKETPLACE_GLOBAL_STATS_API_PATH = '/api/marketplace/stats';
207
229
  const MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS = clampInt(process.env.MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS, 45, 30, 60);
208
230
  const HOME_MARKETPLACE_STATS_CACHE_SECONDS = clampInt(process.env.HOME_MARKETPLACE_STATS_CACHE_SECONDS, 45, 10, 300);
@@ -218,6 +240,14 @@ const SITE_CANONICAL_REDIRECT_ENABLED = parseEnvBool(process.env.SITE_CANONICAL_
218
240
  const SITE_ORIGIN = String(process.env.SITE_ORIGIN || `${SITE_CANONICAL_SCHEME}://${SITE_CANONICAL_HOST}`)
219
241
  .trim()
220
242
  .replace(/\/+$/, '');
243
+ const SITE_COOKIE_DOMAIN = String(process.env.SITE_COOKIE_DOMAIN || SITE_CANONICAL_HOST)
244
+ .trim()
245
+ .toLowerCase()
246
+ .replace(/^https?:\/\//, '')
247
+ .split('/')[0]
248
+ .split(':')[0]
249
+ .replace(/^\.+/, '')
250
+ .replace(/\.+$/, '');
221
251
  const SITEMAP_MAX_PACKS = clampInt(process.env.STICKER_SITEMAP_MAX_PACKS, 45000, 100, 50000);
222
252
  const SITEMAP_CACHE_SECONDS = clampInt(process.env.STICKER_SITEMAP_CACHE_SECONDS, 180, 30, 3600);
223
253
  const SEO_DISCOVERY_LINK_LIMIT = clampInt(process.env.STICKER_SEO_DISCOVERY_LINK_LIMIT, 60, 10, 200);
@@ -295,6 +325,9 @@ const MARKETPLACE_GLOBAL_STATS_CACHE = {
295
325
  pending: null,
296
326
  };
297
327
  const HOME_MARKETPLACE_STATS_CACHE = new Map();
328
+ const CATALOG_LIST_CACHE = new Map();
329
+ const CATALOG_CREATOR_RANKING_CACHE = new Map();
330
+ const CATALOG_PACK_PAYLOAD_CACHE = new Map();
298
331
  const SYSTEM_SUMMARY_CACHE = {
299
332
  expiresAt: 0,
300
333
  value: null,
@@ -324,6 +357,69 @@ const formatDuration = (totalSeconds) => {
324
357
  return days > 0 ? `${days}d ${hhmmss}` : hhmmss;
325
358
  };
326
359
 
360
+ const buildCacheKey = (parts) => JSON.stringify(parts);
361
+
362
+ const getCacheBucket = (cacheMap, key) => {
363
+ let bucket = cacheMap.get(key);
364
+ if (!bucket) {
365
+ bucket = {
366
+ expiresAt: 0,
367
+ value: null,
368
+ pending: null,
369
+ };
370
+ cacheMap.set(key, bucket);
371
+ }
372
+ return bucket;
373
+ };
374
+
375
+ const getCachedSnapshot = async ({
376
+ cacheMap,
377
+ key,
378
+ ttlSeconds,
379
+ staleWhileRefresh = true,
380
+ staleOnError = true,
381
+ load,
382
+ }) => {
383
+ const bucket = getCacheBucket(cacheMap, key);
384
+ const now = Date.now();
385
+ const hasValue = bucket.value !== null;
386
+ const hasFreshValue = hasValue && now < bucket.expiresAt;
387
+
388
+ if (hasFreshValue) {
389
+ return bucket.value;
390
+ }
391
+
392
+ if (!bucket.pending) {
393
+ bucket.pending = Promise.resolve()
394
+ .then(load)
395
+ .then((value) => {
396
+ bucket.value = value;
397
+ bucket.expiresAt = Date.now() + ttlSeconds * 1000;
398
+ return value;
399
+ })
400
+ .finally(() => {
401
+ bucket.pending = null;
402
+ });
403
+ }
404
+
405
+ if (hasValue && staleWhileRefresh) {
406
+ return bucket.value;
407
+ }
408
+
409
+ try {
410
+ return await bucket.pending;
411
+ } catch (error) {
412
+ if (hasValue && staleOnError) return bucket.value;
413
+ throw error;
414
+ }
415
+ };
416
+
417
+ const canUseRankingSnapshotRead = async (subjectKey = 'catalog') =>
418
+ isFeatureEnabled('enable_ranking_snapshot_read', {
419
+ fallback: true,
420
+ subjectKey,
421
+ });
422
+
327
423
  const sendJson = (req, res, statusCode, payload) => {
328
424
  const body = JSON.stringify(payload);
329
425
  res.statusCode = statusCode;
@@ -362,6 +458,22 @@ const toRequestHost = (req) =>
362
458
  .replace(/\.$/, '')
363
459
  .split(':')[0];
364
460
 
461
+ const isIpLiteralHost = (value) => {
462
+ const host = String(value || '').trim().toLowerCase();
463
+ if (!host) return false;
464
+ if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return true;
465
+ return host.includes(':');
466
+ };
467
+
468
+ const resolveCookieDomainForRequest = (req) => {
469
+ if (!SITE_COOKIE_DOMAIN || isIpLiteralHost(SITE_COOKIE_DOMAIN)) return '';
470
+ const requestHost = toRequestHost(req);
471
+ if (!requestHost || isIpLiteralHost(requestHost) || requestHost === 'localhost') return '';
472
+ if (requestHost === SITE_COOKIE_DOMAIN) return SITE_COOKIE_DOMAIN;
473
+ if (requestHost.endsWith(`.${SITE_COOKIE_DOMAIN}`)) return SITE_COOKIE_DOMAIN;
474
+ return '';
475
+ };
476
+
365
477
  const maybeRedirectToCanonicalHost = (req, res, url) => {
366
478
  if (!SITE_CANONICAL_REDIRECT_ENABLED) return false;
367
479
  if (!['GET', 'HEAD'].includes(req.method || '')) return false;
@@ -395,6 +507,33 @@ const parseCookies = (req) => {
395
507
  }, {});
396
508
  };
397
509
 
510
+ const getCookieValuesFromRequest = (req, cookieName) => {
511
+ const target = String(cookieName || '').trim();
512
+ if (!target) return [];
513
+ const raw = String(req?.headers?.cookie || '');
514
+ if (!raw) return [];
515
+
516
+ const values = [];
517
+ for (const chunk of raw.split(';')) {
518
+ const trimmed = String(chunk || '').trim();
519
+ if (!trimmed) continue;
520
+ const separatorIndex = trimmed.indexOf('=');
521
+ if (separatorIndex <= 0) continue;
522
+ const key = trimmed.slice(0, separatorIndex).trim();
523
+ if (key !== target) continue;
524
+ const encodedValue = trimmed.slice(separatorIndex + 1).trim();
525
+ if (!encodedValue) continue;
526
+ let decodedValue = encodedValue;
527
+ try {
528
+ decodedValue = decodeURIComponent(encodedValue);
529
+ } catch {}
530
+ const normalizedValue = String(decodedValue || '').trim();
531
+ if (!normalizedValue) continue;
532
+ if (!values.includes(normalizedValue)) values.push(normalizedValue);
533
+ }
534
+ return values;
535
+ };
536
+
398
537
  const isRequestSecure = (req) => {
399
538
  const proto = String(req?.headers?.['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase();
400
539
  if (proto) return proto === 'https';
@@ -503,6 +642,11 @@ const runSqlTransaction = async (handler) => {
503
642
  const buildCookieString = (name, value, req, options = {}) => {
504
643
  const parts = [`${name}=${encodeURIComponent(String(value ?? ''))}`];
505
644
  parts.push(`Path=${options.path || '/'}`);
645
+ const cookieDomain =
646
+ options.domain === false
647
+ ? ''
648
+ : String(options.domain || resolveCookieDomainForRequest(req)).trim();
649
+ if (cookieDomain) parts.push(`Domain=${cookieDomain}`);
506
650
  if (options.httpOnly !== false) parts.push('HttpOnly');
507
651
  parts.push(`SameSite=${options.sameSite || 'Lax'}`);
508
652
  if (isRequestSecure(req)) parts.push('Secure');
@@ -914,11 +1058,16 @@ const pruneExpiredGoogleSessions = () => {
914
1058
  }
915
1059
  };
916
1060
 
917
- const getGoogleWebSessionTokenFromRequest = (req) => {
1061
+ const getGoogleWebSessionTokensFromRequest = (req) => {
1062
+ const direct = getCookieValuesFromRequest(req, GOOGLE_WEB_SESSION_COOKIE_NAME);
1063
+ if (direct.length > 0) return direct;
918
1064
  const cookies = parseCookies(req);
919
- return String(cookies[GOOGLE_WEB_SESSION_COOKIE_NAME] || '').trim();
1065
+ const fallback = String(cookies[GOOGLE_WEB_SESSION_COOKIE_NAME] || '').trim();
1066
+ return fallback ? [fallback] : [];
920
1067
  };
921
1068
 
1069
+ const getGoogleWebSessionTokenFromRequest = (req) => getGoogleWebSessionTokensFromRequest(req)[0] || '';
1070
+
922
1071
  const normalizeGoogleWebSessionRow = (row) => {
923
1072
  if (!row || typeof row !== 'object') return null;
924
1073
  const token = String(row.session_token || '').trim();
@@ -1574,6 +1723,8 @@ const pruneExpiredAdminPanelSessions = () => {
1574
1723
  };
1575
1724
 
1576
1725
  const getAdminPanelSessionTokenFromRequest = (req) => {
1726
+ const direct = getCookieValuesFromRequest(req, ADMIN_PANEL_SESSION_COOKIE_NAME);
1727
+ if (direct.length > 0) return direct[0];
1577
1728
  const cookies = parseCookies(req);
1578
1729
  return String(cookies[ADMIN_PANEL_SESSION_COOKIE_NAME] || '').trim();
1579
1730
  };
@@ -1585,6 +1736,14 @@ const clearAdminPanelSessionCookie = (req, res) => {
1585
1736
  maxAgeSeconds: 0,
1586
1737
  }),
1587
1738
  );
1739
+ // Also clear host-only variant (legacy cookie written without Domain).
1740
+ appendSetCookie(
1741
+ res,
1742
+ buildCookieString(ADMIN_PANEL_SESSION_COOKIE_NAME, '', req, {
1743
+ maxAgeSeconds: 0,
1744
+ domain: false,
1745
+ }),
1746
+ );
1588
1747
  };
1589
1748
 
1590
1749
  const createAdminPanelSession = (googleSession, { role = 'owner' } = {}) => {
@@ -1732,61 +1891,67 @@ const activateGoogleWebSession = (session) => {
1732
1891
 
1733
1892
  const resolveGoogleWebSessionFromRequest = async (req) => {
1734
1893
  pruneExpiredGoogleSessions();
1735
- const sessionToken = getGoogleWebSessionTokenFromRequest(req);
1736
- if (!sessionToken) return null;
1737
- const session = webGoogleSessionMap.get(sessionToken);
1738
- if (session) {
1894
+ const sessionTokens = getGoogleWebSessionTokensFromRequest(req);
1895
+ if (!sessionTokens.length) return null;
1896
+
1897
+ for (const sessionToken of sessionTokens) {
1898
+ const session = webGoogleSessionMap.get(sessionToken);
1899
+ if (!session) continue;
1739
1900
  if (Number(session.expiresAt || 0) <= Date.now()) {
1740
1901
  webGoogleSessionMap.delete(sessionToken);
1741
- } else {
1742
- const now = Date.now();
1743
- session.lastSeenAt = now;
1744
- if (now - Number(session.lastDbTouchAt || 0) >= GOOGLE_WEB_SESSION_DB_TOUCH_INTERVAL_MS) {
1745
- session.lastDbTouchAt = now;
1746
- void touchGoogleWebSessionSeenInDb(sessionToken).catch((error) => {
1747
- logger.warn('Falha ao atualizar last_seen da sessão Google web.', {
1748
- action: 'sticker_pack_google_web_session_touch_failed',
1749
- error: error?.message,
1750
- });
1751
- });
1752
- void touchGoogleWebUserSeenInDb(session.sub).catch(() => {});
1753
- }
1754
- try {
1755
- await assertGoogleIdentityNotBanned({
1756
- sub: session.sub,
1757
- email: session.email,
1758
- ownerJid: session.ownerJid,
1902
+ continue;
1903
+ }
1904
+
1905
+ const now = Date.now();
1906
+ session.lastSeenAt = now;
1907
+ if (now - Number(session.lastDbTouchAt || 0) >= GOOGLE_WEB_SESSION_DB_TOUCH_INTERVAL_MS) {
1908
+ session.lastDbTouchAt = now;
1909
+ void touchGoogleWebSessionSeenInDb(sessionToken).catch((error) => {
1910
+ logger.warn('Falha ao atualizar last_seen da sessão Google web.', {
1911
+ action: 'sticker_pack_google_web_session_touch_failed',
1912
+ error: error?.message,
1759
1913
  });
1760
- } catch (banError) {
1761
- webGoogleSessionMap.delete(sessionToken);
1762
- void deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1763
- return null;
1764
- }
1765
- return session;
1914
+ });
1915
+ void touchGoogleWebUserSeenInDb(session.sub).catch(() => {});
1766
1916
  }
1767
- }
1768
- try {
1769
- const persistedSession = await findGoogleWebSessionInDbByToken(sessionToken);
1770
- if (!persistedSession) return null;
1771
1917
  try {
1772
1918
  await assertGoogleIdentityNotBanned({
1773
- sub: persistedSession.sub,
1774
- email: persistedSession.email,
1775
- ownerJid: persistedSession.ownerJid,
1919
+ sub: session.sub,
1920
+ email: session.email,
1921
+ ownerJid: session.ownerJid,
1776
1922
  });
1923
+ return session;
1777
1924
  } catch {
1778
- await deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1779
- return null;
1925
+ webGoogleSessionMap.delete(sessionToken);
1926
+ void deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1780
1927
  }
1781
- webGoogleSessionMap.set(sessionToken, persistedSession);
1782
- return persistedSession;
1783
- } catch (error) {
1784
- logger.warn('Falha ao resolver sessão Google web no banco.', {
1785
- action: 'sticker_pack_google_web_session_db_resolve_failed',
1786
- error: error?.message,
1787
- });
1788
- return null;
1789
1928
  }
1929
+
1930
+ for (const sessionToken of sessionTokens) {
1931
+ try {
1932
+ const persistedSession = await findGoogleWebSessionInDbByToken(sessionToken);
1933
+ if (!persistedSession) continue;
1934
+ try {
1935
+ await assertGoogleIdentityNotBanned({
1936
+ sub: persistedSession.sub,
1937
+ email: persistedSession.email,
1938
+ ownerJid: persistedSession.ownerJid,
1939
+ });
1940
+ } catch {
1941
+ await deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1942
+ continue;
1943
+ }
1944
+ webGoogleSessionMap.set(sessionToken, persistedSession);
1945
+ return persistedSession;
1946
+ } catch (error) {
1947
+ logger.warn('Falha ao resolver sessão Google web no banco.', {
1948
+ action: 'sticker_pack_google_web_session_db_resolve_failed',
1949
+ error: error?.message,
1950
+ });
1951
+ }
1952
+ }
1953
+
1954
+ return null;
1790
1955
  };
1791
1956
 
1792
1957
  const clearGoogleWebSessionCookie = (req, res) => {
@@ -1796,6 +1961,14 @@ const clearGoogleWebSessionCookie = (req, res) => {
1796
1961
  maxAgeSeconds: 0,
1797
1962
  }),
1798
1963
  );
1964
+ // Also clear host-only variant (legacy cookie written without Domain).
1965
+ appendSetCookie(
1966
+ res,
1967
+ buildCookieString(GOOGLE_WEB_SESSION_COOKIE_NAME, '', req, {
1968
+ maxAgeSeconds: 0,
1969
+ domain: false,
1970
+ }),
1971
+ );
1799
1972
  };
1800
1973
 
1801
1974
  const sendAsset = (req, res, buffer, mimetype = 'image/webp') => {
@@ -2146,7 +2319,9 @@ const resolveWebCreateOwnerJid = async (explicitOwner = '') => {
2146
2319
  const resolvedAdminJid = await resolveAdminJid();
2147
2320
  const fromAdmin = toOwnerJid(resolvedAdminJid);
2148
2321
  if (fromAdmin) return fromAdmin;
2149
- } catch {}
2322
+ } catch {
2323
+ // Ignore fallback errors while resolving owner identity.
2324
+ }
2150
2325
 
2151
2326
  const adminCandidates = [
2152
2327
  getAdminRawValue(),
@@ -2316,14 +2491,18 @@ const resolveSupportAdminPhone = async () => {
2316
2491
  const resolvedFromLidMap = await resolveUserId(extractUserIdInfo(adminRaw));
2317
2492
  const resolvedPhoneFromLidMap = isPlausibleWhatsAppPhone(getJidUser(resolvedFromLidMap || ''));
2318
2493
  if (resolvedPhoneFromLidMap) return resolvedPhoneFromLidMap;
2319
- } catch {}
2494
+ } catch {
2495
+ // Ignore and fallback to other admin sources.
2496
+ }
2320
2497
  }
2321
2498
 
2322
2499
  try {
2323
2500
  const resolvedAdminJid = await resolveAdminJid();
2324
2501
  const resolvedPhone = isPlausibleWhatsAppPhone(getJidUser(resolvedAdminJid || ''));
2325
2502
  if (resolvedPhone) return resolvedPhone;
2326
- } catch {}
2503
+ } catch {
2504
+ // Ignore and fallback to static admin phone sources.
2505
+ }
2327
2506
 
2328
2507
  const rawPhone = isPlausibleWhatsAppPhone(getJidUser(adminRaw) || adminRaw);
2329
2508
  if (rawPhone) return rawPhone;
@@ -2725,6 +2904,10 @@ const hydrateMarketplaceEntries = async (packs, { includeItems = true, driftSnap
2725
2904
  const packIds = packs.map((pack) => pack.id);
2726
2905
  const engagementByPackId = await listStickerPackEngagementByPackIds(packIds);
2727
2906
  const interactionStatsByPackId = await listStickerPackInteractionStatsByPackIds(packIds);
2907
+ const useSnapshot = await canUseRankingSnapshotRead(`hydrate:${packIds.length}:${includeItems ? 1 : 0}`);
2908
+ const snapshotByPackId = useSnapshot
2909
+ ? await listStickerPackScoreSnapshotsByPackIds(packIds).catch(() => new Map())
2910
+ : new Map();
2728
2911
 
2729
2912
  const entries = [];
2730
2913
  const packClassificationById = new Map();
@@ -2743,14 +2926,24 @@ const hydrateMarketplaceEntries = async (packs, { includeItems = true, driftSnap
2743
2926
  const packMetadata = parsePackDescriptionMetadata(pack.description);
2744
2927
  const decoratedClassification = decoratePackClassificationSummary(packClassification);
2745
2928
  const mergedPackTags = mergeUniqueTags(decoratedClassification?.tags || [], packMetadata.tags);
2746
- const signals = computePackSignals({
2747
- pack: { ...pack, items },
2748
- engagement,
2749
- packClassification,
2750
- itemClassifications: orderedClassifications,
2751
- interactionStats,
2752
- scoringWeights: driftSnapshot?.weights || null,
2753
- });
2929
+ const snapshot = snapshotByPackId.get(pack.id);
2930
+ const signals = snapshot?.signals
2931
+ ? {
2932
+ ...snapshot.signals,
2933
+ ranking_score: Number(snapshot?.signals?.ranking_score || 0),
2934
+ pack_score: Number(snapshot?.signals?.pack_score || 0),
2935
+ trend_score: Number(snapshot?.signals?.trend_score || 0),
2936
+ nsfw_level: String(snapshot?.signals?.nsfw_level || 'safe'),
2937
+ sensitive_content: Boolean(snapshot?.signals?.sensitive_content),
2938
+ }
2939
+ : computePackSignals({
2940
+ pack: { ...pack, items },
2941
+ engagement,
2942
+ packClassification,
2943
+ itemClassifications: orderedClassifications,
2944
+ interactionStats,
2945
+ scoringWeights: driftSnapshot?.weights || null,
2946
+ });
2754
2947
 
2755
2948
  const entry = {
2756
2949
  pack,
@@ -3321,95 +3514,118 @@ const handleListRequest = async (req, res, url) => {
3321
3514
  const limit = clampInt(url.searchParams.get('limit'), DEFAULT_LIST_LIMIT, 1, MAX_LIST_LIMIT);
3322
3515
  const offset = clampInt(url.searchParams.get('offset'), 0, 0, 100000);
3323
3516
  const normalizedIntent = normalizeCategoryToken(intent).replace(/-/g, '_');
3324
- const batchLimit = Math.max(limit, Math.min(MAX_LIST_LIMIT, 24));
3325
- const maxPagesToScan = 8;
3326
3517
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
3327
3518
  const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
3328
- const seenPackIds = new Set();
3329
- const collectedEntries = [];
3330
- const driftSnapshot = await getMarketplaceDriftSnapshot();
3331
- let sourceHasMore = true;
3332
- let cursorOffset = offset;
3333
- let pagesScanned = 0;
3519
+ const cacheKey = buildCacheKey([
3520
+ 'list',
3521
+ q,
3522
+ visibility,
3523
+ sort,
3524
+ categories.join(','),
3525
+ normalizedIntent,
3526
+ includeSensitive ? 1 : 0,
3527
+ limit,
3528
+ offset,
3529
+ hasNsfwAccess ? 1 : 0,
3530
+ ]);
3531
+ const payload = await getCachedSnapshot({
3532
+ cacheMap: CATALOG_LIST_CACHE,
3533
+ key: cacheKey,
3534
+ ttlSeconds: CATALOG_LIST_CACHE_SECONDS,
3535
+ staleWhileRefresh: true,
3536
+ staleOnError: true,
3537
+ load: async () => {
3538
+ const batchLimit = Math.max(limit, Math.min(MAX_LIST_LIMIT, 24));
3539
+ const maxPagesToScan = 8;
3540
+ const seenPackIds = new Set();
3541
+ const collectedEntries = [];
3542
+ const driftSnapshot = await getMarketplaceDriftSnapshot();
3543
+ let sourceHasMore = true;
3544
+ let cursorOffset = offset;
3545
+ let pagesScanned = 0;
3546
+
3547
+ while (collectedEntries.length < limit && sourceHasMore && pagesScanned < maxPagesToScan) {
3548
+ pagesScanned += 1;
3549
+ const { packs, hasMore } = await listStickerPacksForCatalog({
3550
+ visibility,
3551
+ search: q,
3552
+ limit: batchLimit,
3553
+ offset: cursorOffset,
3554
+ });
3555
+ sourceHasMore = hasMore;
3556
+ cursorOffset += batchLimit;
3557
+ if (!packs.length) break;
3558
+
3559
+ const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
3560
+ const entriesClassified = STICKER_CATALOG_ONLY_CLASSIFIED
3561
+ ? entries.filter((entry) => isPackClassified(entry.packClassification))
3562
+ : entries;
3563
+ const entriesByCategory = categories.length
3564
+ ? entriesClassified.filter((entry) => hasAnyCategory(entry.packClassification?.tags || [], categories))
3565
+ : entriesClassified;
3566
+ const entriesBySensitivity = includeSensitive
3567
+ ? entriesByCategory
3568
+ : entriesByCategory.filter((entry) => entry.signals?.nsfw_level === 'safe');
3569
+ const entriesByIntent = intent
3570
+ ? entriesBySensitivity.filter((entry) => classifyPackIntent(entry) === normalizedIntent)
3571
+ : entriesBySensitivity;
3572
+ const sortedEntries = [...entriesByIntent].sort((left, right) => {
3573
+ const completenessDelta = compareEntriesByPackCompleteness(left, right);
3574
+ if (completenessDelta !== 0) return completenessDelta;
3575
+ if (sort === 'recent') {
3576
+ return Date.parse(right?.pack?.created_at || right?.pack?.updated_at || 0) - Date.parse(left?.pack?.created_at || left?.pack?.updated_at || 0);
3577
+ }
3578
+ if (sort === 'likes') {
3579
+ return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3580
+ }
3581
+ if (sort === 'downloads') {
3582
+ return Number(right?.engagement?.open_count || 0) - Number(left?.engagement?.open_count || 0);
3583
+ }
3584
+ if (sort === 'comments') {
3585
+ const commentDelta = Number(right?.engagement?.comment_count || 0) - Number(left?.engagement?.comment_count || 0);
3586
+ if (commentDelta !== 0) return commentDelta;
3587
+ return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3588
+ }
3589
+ if (sort === 'trending') {
3590
+ const trendDelta = Number(right?.signals?.trend_score || 0) - Number(left?.signals?.trend_score || 0);
3591
+ if (trendDelta !== 0) return trendDelta;
3592
+ }
3593
+ const leftScore = Number(left?.signals?.ranking_score || 0);
3594
+ const rightScore = Number(right?.signals?.ranking_score || 0);
3595
+ if (rightScore !== leftScore) return rightScore - leftScore;
3596
+ return Date.parse(right?.pack?.updated_at || 0) - Date.parse(left?.pack?.updated_at || 0);
3597
+ });
3334
3598
 
3335
- while (collectedEntries.length < limit && sourceHasMore && pagesScanned < maxPagesToScan) {
3336
- pagesScanned += 1;
3337
- const { packs, hasMore } = await listStickerPacksForCatalog({
3338
- visibility,
3339
- search: q,
3340
- limit: batchLimit,
3341
- offset: cursorOffset,
3342
- });
3343
- sourceHasMore = hasMore;
3344
- cursorOffset += batchLimit;
3345
- if (!packs.length) break;
3346
-
3347
- const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
3348
- const entriesClassified = STICKER_CATALOG_ONLY_CLASSIFIED
3349
- ? entries.filter((entry) => isPackClassified(entry.packClassification))
3350
- : entries;
3351
- const entriesByCategory = categories.length
3352
- ? entriesClassified.filter((entry) => hasAnyCategory(entry.packClassification?.tags || [], categories))
3353
- : entriesClassified;
3354
- const entriesBySensitivity = includeSensitive
3355
- ? entriesByCategory
3356
- : entriesByCategory.filter((entry) => entry.signals?.nsfw_level === 'safe');
3357
- const entriesByIntent = intent
3358
- ? entriesBySensitivity.filter((entry) => classifyPackIntent(entry) === normalizedIntent)
3359
- : entriesBySensitivity;
3360
- const sortedEntries = [...entriesByIntent].sort((left, right) => {
3361
- const completenessDelta = compareEntriesByPackCompleteness(left, right);
3362
- if (completenessDelta !== 0) return completenessDelta;
3363
- if (sort === 'recent') {
3364
- return Date.parse(right?.pack?.created_at || right?.pack?.updated_at || 0) - Date.parse(left?.pack?.created_at || left?.pack?.updated_at || 0);
3365
- }
3366
- if (sort === 'likes') {
3367
- return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3368
- }
3369
- if (sort === 'downloads') {
3370
- return Number(right?.engagement?.open_count || 0) - Number(left?.engagement?.open_count || 0);
3371
- }
3372
- if (sort === 'comments') {
3373
- const commentDelta = Number(right?.engagement?.comment_count || 0) - Number(left?.engagement?.comment_count || 0);
3374
- if (commentDelta !== 0) return commentDelta;
3375
- return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3376
- }
3377
- if (sort === 'trending') {
3378
- const trendDelta = Number(right?.signals?.trend_score || 0) - Number(left?.signals?.trend_score || 0);
3379
- if (trendDelta !== 0) return trendDelta;
3599
+ for (const entry of sortedEntries) {
3600
+ if (!entry?.pack?.id) continue;
3601
+ if (seenPackIds.has(entry.pack.id)) continue;
3602
+ seenPackIds.add(entry.pack.id);
3603
+ collectedEntries.push(entry);
3604
+ if (collectedEntries.length >= limit) break;
3605
+ }
3380
3606
  }
3381
- const leftScore = Number(left?.signals?.ranking_score || 0);
3382
- const rightScore = Number(right?.signals?.ranking_score || 0);
3383
- if (rightScore !== leftScore) return rightScore - leftScore;
3384
- return Date.parse(right?.pack?.updated_at || 0) - Date.parse(left?.pack?.updated_at || 0);
3385
- });
3386
3607
 
3387
- for (const entry of sortedEntries) {
3388
- if (!entry?.pack?.id) continue;
3389
- if (seenPackIds.has(entry.pack.id)) continue;
3390
- seenPackIds.add(entry.pack.id);
3391
- collectedEntries.push(entry);
3392
- if (collectedEntries.length >= limit) break;
3393
- }
3394
- }
3395
-
3396
- sendJson(req, res, 200, {
3397
- data: collectedEntries.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
3398
- pagination: {
3399
- limit,
3400
- offset,
3401
- has_more: sourceHasMore,
3402
- next_offset: sourceHasMore ? cursorOffset : null,
3403
- },
3404
- filters: {
3405
- q,
3406
- visibility,
3407
- sort,
3408
- categories,
3409
- intent: intent || null,
3410
- include_sensitive: includeSensitive,
3608
+ return {
3609
+ data: collectedEntries.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
3610
+ pagination: {
3611
+ limit,
3612
+ offset,
3613
+ has_more: sourceHasMore,
3614
+ next_offset: sourceHasMore ? cursorOffset : null,
3615
+ },
3616
+ filters: {
3617
+ q,
3618
+ visibility,
3619
+ sort,
3620
+ categories,
3621
+ intent: intent || null,
3622
+ include_sensitive: includeSensitive,
3623
+ },
3624
+ };
3411
3625
  },
3412
3626
  });
3627
+
3628
+ sendJson(req, res, 200, payload);
3413
3629
  };
3414
3630
 
3415
3631
  const handleIntentCollectionsRequest = async (req, res, url) => {
@@ -3647,9 +3863,9 @@ const handleGoogleAuthSessionRequest = async (req, res) => {
3647
3863
  }
3648
3864
 
3649
3865
  if (req.method === 'DELETE') {
3650
- const token = getGoogleWebSessionTokenFromRequest(req);
3651
- if (token) webGoogleSessionMap.delete(token);
3652
- if (token) {
3866
+ const tokens = getGoogleWebSessionTokensFromRequest(req);
3867
+ for (const token of tokens) {
3868
+ webGoogleSessionMap.delete(token);
3653
3869
  await deleteGoogleWebSessionFromDb(token).catch((error) => {
3654
3870
  logger.warn('Falha ao remover sessão Google web do banco.', {
3655
3871
  action: 'sticker_pack_google_web_session_db_delete_failed',
@@ -4090,6 +4306,9 @@ const invalidateStickerCatalogDerivedCaches = () => {
4090
4306
  GLOBAL_RANK_CACHE.value = null;
4091
4307
  GLOBAL_RANK_CACHE.pending = null;
4092
4308
  HOME_MARKETPLACE_STATS_CACHE.clear();
4309
+ CATALOG_LIST_CACHE.clear();
4310
+ CATALOG_CREATOR_RANKING_CACHE.clear();
4311
+ CATALOG_PACK_PAYLOAD_CACHE.clear();
4093
4312
  SYSTEM_SUMMARY_CACHE.expiresAt = 0;
4094
4313
  SYSTEM_SUMMARY_CACHE.value = null;
4095
4314
  SYSTEM_SUMMARY_CACHE.pending = null;
@@ -5753,46 +5972,63 @@ const handleCreatorRankingRequest = async (req, res, url) => {
5753
5972
  const limit = clampInt(url.searchParams.get('limit'), 50, 5, 200);
5754
5973
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
5755
5974
  const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
5756
-
5757
- const { packs } = await listStickerPacksForCatalog({
5975
+ const cacheKey = buildCacheKey([
5976
+ 'creator_ranking',
5758
5977
  visibility,
5759
- search: q,
5760
- limit: 120,
5761
- offset: 0,
5762
- });
5763
- const driftSnapshot = await getMarketplaceDriftSnapshot();
5764
- const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
5765
- const ranking = buildCreatorRanking(
5766
- STICKER_CATALOG_ONLY_CLASSIFIED ? entries.filter((entry) => isPackClassified(entry.packClassification)) : entries,
5767
- { limit },
5768
- );
5978
+ q,
5979
+ limit,
5980
+ hasNsfwAccess ? 1 : 0,
5981
+ ]);
5982
+ const payload = await getCachedSnapshot({
5983
+ cacheMap: CATALOG_CREATOR_RANKING_CACHE,
5984
+ key: cacheKey,
5985
+ ttlSeconds: CATALOG_CREATOR_RANKING_CACHE_SECONDS,
5986
+ staleWhileRefresh: true,
5987
+ staleOnError: true,
5988
+ load: async () => {
5989
+ const { packs } = await listStickerPacksForCatalog({
5990
+ visibility,
5991
+ search: q,
5992
+ limit: 120,
5993
+ offset: 0,
5994
+ });
5995
+ const driftSnapshot = await getMarketplaceDriftSnapshot();
5996
+ const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
5997
+ const ranking = buildCreatorRanking(
5998
+ STICKER_CATALOG_ONLY_CLASSIFIED ? entries.filter((entry) => isPackClassified(entry.packClassification)) : entries,
5999
+ { limit },
6000
+ );
5769
6001
 
5770
- sendJson(req, res, 200, {
5771
- data: ranking.map((creator) => ({
5772
- creator_score: Number(
5773
- (
5774
- Number(creator.avg_pack_score || 0) * 0.45 +
5775
- Number(creator.total_likes || 0) * 0.0008 +
5776
- Number(creator.total_opens || 0) * 0.00015
5777
- ).toFixed(6),
5778
- ),
5779
- publisher: creator.publisher,
5780
- verified: Boolean(creator.verified),
5781
- badges: creator.verified ? ['verified_creator'] : [],
5782
- stats: {
5783
- packs_count: Number(creator.packs_count || 0),
5784
- total_likes: Number(creator.total_likes || 0),
5785
- total_opens: Number(creator.total_opens || 0),
5786
- avg_pack_score: Number(creator.avg_pack_score || 0),
5787
- },
5788
- top_pack: creator.top_pack ? toSummaryEntry(creator.top_pack, { hideSensitiveCover: !hasNsfwAccess }) : null,
5789
- })),
5790
- filters: {
5791
- visibility,
5792
- q,
5793
- limit,
6002
+ return {
6003
+ data: ranking.map((creator) => ({
6004
+ creator_score: Number(
6005
+ (
6006
+ Number(creator.avg_pack_score || 0) * 0.45 +
6007
+ Number(creator.total_likes || 0) * 0.0008 +
6008
+ Number(creator.total_opens || 0) * 0.00015
6009
+ ).toFixed(6),
6010
+ ),
6011
+ publisher: creator.publisher,
6012
+ verified: Boolean(creator.verified),
6013
+ badges: creator.verified ? ['verified_creator'] : [],
6014
+ stats: {
6015
+ packs_count: Number(creator.packs_count || 0),
6016
+ total_likes: Number(creator.total_likes || 0),
6017
+ total_opens: Number(creator.total_opens || 0),
6018
+ avg_pack_score: Number(creator.avg_pack_score || 0),
6019
+ },
6020
+ top_pack: creator.top_pack ? toSummaryEntry(creator.top_pack, { hideSensitiveCover: !hasNsfwAccess }) : null,
6021
+ })),
6022
+ filters: {
6023
+ visibility,
6024
+ q,
6025
+ limit,
6026
+ },
6027
+ };
5794
6028
  },
5795
6029
  });
6030
+
6031
+ sendJson(req, res, 200, payload);
5796
6032
  };
5797
6033
 
5798
6034
  const handleRecommendationsRequest = async (req, res, url) => {
@@ -6934,42 +7170,60 @@ const handlePublicDataAssetRequest = async (req, res, pathname) => {
6934
7170
  };
6935
7171
 
6936
7172
  const fetchPublicPackPayload = async (normalizedPackKey) => {
6937
- const pack = await findStickerPackByPackKey(normalizedPackKey);
6938
- if (!pack || !isPackPubliclyVisible(pack)) return null;
6939
-
6940
- const items = await listStickerPackItems(pack.id);
6941
- const stickerIds = items.map((item) => item.sticker_id);
6942
- const [classifications, packClassification, engagement] = await Promise.all([
6943
- listStickerClassificationsByAssetIds(stickerIds),
6944
- getPackClassificationSummaryByAssetIds(stickerIds),
6945
- getStickerPackEngagementByPackId(pack.id),
6946
- ]);
7173
+ const cacheKey = buildCacheKey(['pack_payload', normalizedPackKey]);
7174
+ return getCachedSnapshot({
7175
+ cacheMap: CATALOG_PACK_PAYLOAD_CACHE,
7176
+ key: cacheKey,
7177
+ ttlSeconds: CATALOG_PACK_PAYLOAD_CACHE_SECONDS,
7178
+ staleWhileRefresh: true,
7179
+ staleOnError: true,
7180
+ load: async () => {
7181
+ const pack = await findStickerPackByPackKey(normalizedPackKey);
7182
+ if (!pack || !isPackPubliclyVisible(pack)) return null;
7183
+
7184
+ const items = await listStickerPackItems(pack.id);
7185
+ const stickerIds = items.map((item) => item.sticker_id);
7186
+ const [classifications, packClassification, engagement] = await Promise.all([
7187
+ listStickerClassificationsByAssetIds(stickerIds),
7188
+ getPackClassificationSummaryByAssetIds(stickerIds),
7189
+ getStickerPackEngagementByPackId(pack.id),
7190
+ ]);
7191
+
7192
+ if (STICKER_CATALOG_ONLY_CLASSIFIED && !isPackClassified(packClassification)) {
7193
+ return null;
7194
+ }
6947
7195
 
6948
- if (STICKER_CATALOG_ONLY_CLASSIFIED && !isPackClassified(packClassification)) {
6949
- return null;
6950
- }
7196
+ const [interactionStatsByPack, driftSnapshot, snapshotByPackId] = await Promise.all([
7197
+ listStickerPackInteractionStatsByPackIds([pack.id]),
7198
+ getMarketplaceDriftSnapshot(),
7199
+ canUseRankingSnapshotRead(`pack_payload:${pack.id}`)
7200
+ .then((enabled) => (enabled ? listStickerPackScoreSnapshotsByPackIds([pack.id]) : new Map()))
7201
+ .catch(() => new Map()),
7202
+ ]);
7203
+ const byAssetClassification = new Map(classifications.map((entry) => [entry.asset_id, entry]));
7204
+ const orderedClassifications = stickerIds.map((stickerId) => byAssetClassification.get(stickerId)).filter(Boolean);
7205
+ const snapshot = snapshotByPackId.get(pack.id);
7206
+ const signals = snapshot?.signals
7207
+ ? snapshot.signals
7208
+ : computePackSignals({
7209
+ pack: { ...pack, items },
7210
+ engagement,
7211
+ packClassification,
7212
+ itemClassifications: orderedClassifications,
7213
+ interactionStats: interactionStatsByPack.get(pack.id) || null,
7214
+ scoringWeights: driftSnapshot.weights,
7215
+ });
6951
7216
 
6952
- const interactionStatsByPack = await listStickerPackInteractionStatsByPackIds([pack.id]);
6953
- const driftSnapshot = await getMarketplaceDriftSnapshot();
6954
- const byAssetClassification = new Map(classifications.map((entry) => [entry.asset_id, entry]));
6955
- const orderedClassifications = stickerIds.map((stickerId) => byAssetClassification.get(stickerId)).filter(Boolean);
6956
- const signals = computePackSignals({
6957
- pack: { ...pack, items },
6958
- engagement,
6959
- packClassification,
6960
- itemClassifications: orderedClassifications,
6961
- interactionStats: interactionStatsByPack.get(pack.id) || null,
6962
- scoringWeights: driftSnapshot.weights,
7217
+ return {
7218
+ pack,
7219
+ items,
7220
+ byAssetClassification,
7221
+ packClassification,
7222
+ engagement,
7223
+ signals,
7224
+ };
7225
+ },
6963
7226
  });
6964
-
6965
- return {
6966
- pack,
6967
- items,
6968
- byAssetClassification,
6969
- packClassification,
6970
- engagement,
6971
- signals,
6972
- };
6973
7227
  };
6974
7228
 
6975
7229
  const handleDetailsRequest = async (req, res, packKey, url) => {
@@ -7060,6 +7314,23 @@ const handleAssetRequest = async (req, res, packKey, stickerToken) => {
7060
7314
  return;
7061
7315
  }
7062
7316
  }
7317
+
7318
+ const externalAssetUrl = await getStickerAssetExternalUrl(item.asset, {
7319
+ secure: true,
7320
+ expiresInSeconds: Math.max(60, Math.min(3600, Number(process.env.STICKER_OBJECT_STORAGE_SIGNED_URL_TTL_SECONDS) || 300)),
7321
+ }).catch(() => null);
7322
+ if (externalAssetUrl) {
7323
+ res.statusCode = 302;
7324
+ res.setHeader('Location', externalAssetUrl);
7325
+ res.setHeader('Cache-Control', 'private, max-age=45');
7326
+ if (req.method === 'HEAD') {
7327
+ res.end();
7328
+ return;
7329
+ }
7330
+ res.end();
7331
+ return;
7332
+ }
7333
+
7063
7334
  if (decorated) {
7064
7335
  res.setHeader('X-Sticker-Category', String(decorated?.category || 'unknown'));
7065
7336
  res.setHeader('X-Sticker-NSFW', decorated?.is_nsfw ? '1' : '0');
@@ -7667,323 +7938,60 @@ const handleAdminBanRevokeRequest = async (req, res, banId) => {
7667
7938
  }
7668
7939
  };
7669
7940
 
7670
- const handleCatalogApiRequest = async (req, res, pathname, url) => {
7671
- if (pathname === `${STICKER_API_BASE_PATH}/create`) {
7672
- if (req.method !== 'POST') {
7673
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7674
- return true;
7675
- }
7676
- await handleCreatePackRequest(req, res);
7677
- return true;
7678
- }
7679
-
7680
- if (pathname === `${STICKER_API_BASE_PATH}/auth/google/session`) {
7681
- await handleGoogleAuthSessionRequest(req, res);
7682
- return true;
7683
- }
7684
-
7685
- if (pathname === `${STICKER_API_BASE_PATH}/me`) {
7686
- await handleMyProfileRequest(req, res, url);
7687
- return true;
7688
- }
7689
-
7690
- if (pathname === `${STICKER_API_BASE_PATH}/admin/session`) {
7691
- await handleAdminPanelSessionRequest(req, res);
7692
- return true;
7693
- }
7694
-
7695
- if (pathname === STICKER_API_BASE_PATH) {
7696
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7697
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7698
- return true;
7699
- }
7700
- await handleListRequest(req, res, url);
7701
- return true;
7702
- }
7703
-
7704
- if (pathname === `${STICKER_API_BASE_PATH}/intents`) {
7705
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7706
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7707
- return true;
7708
- }
7709
- await handleIntentCollectionsRequest(req, res, url);
7710
- return true;
7711
- }
7712
-
7713
- if (pathname === `${STICKER_API_BASE_PATH}/creators`) {
7714
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7715
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7716
- return true;
7717
- }
7718
- await handleCreatorRankingRequest(req, res, url);
7719
- return true;
7720
- }
7721
-
7722
- if (pathname === `${STICKER_API_BASE_PATH}/recommendations`) {
7723
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7724
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7725
- return true;
7726
- }
7727
- await handleRecommendationsRequest(req, res, url);
7728
- return true;
7729
- }
7730
-
7731
- if (pathname === `${STICKER_API_BASE_PATH}/stats`) {
7732
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7733
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7734
- return true;
7735
- }
7736
- await handleMarketplaceStatsRequest(req, res, url);
7737
- return true;
7738
- }
7739
-
7740
- if (pathname === `${STICKER_API_BASE_PATH}/create-config`) {
7741
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7742
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7743
- return true;
7744
- }
7745
- await handleCreatePackConfigRequest(req, res);
7746
- return true;
7747
- }
7748
-
7749
- if (pathname === STICKER_ORPHAN_API_PATH) {
7750
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7751
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7752
- return true;
7753
- }
7754
- await handleOrphanStickerListRequest(req, res, url);
7755
- return true;
7756
- }
7757
-
7758
- const suffix = pathname.slice(STICKER_API_BASE_PATH.length).replace(/^\/+/, '');
7759
- if (!suffix) return false;
7760
-
7761
- const segments = suffix.split('/').filter(Boolean).map((segment) => {
7762
- try {
7763
- return decodeURIComponent(segment);
7764
- } catch {
7765
- return segment;
7766
- }
7767
- });
7768
-
7769
- if (segments.length === 1 && segments[0] === 'data-files') {
7770
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7771
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7772
- return true;
7773
- }
7774
- await handleDataFileListRequest(req, res, url);
7775
- return true;
7776
- }
7777
-
7778
- if (segments.length === 1 && segments[0] === 'system-summary') {
7779
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7780
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7781
- return true;
7782
- }
7783
- await handleSystemSummaryRequest(req, res);
7784
- return true;
7785
- }
7786
-
7787
- if (segments.length === 1 && segments[0] === 'project-summary') {
7788
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7789
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7790
- return true;
7791
- }
7792
- await handleGitHubProjectSummaryRequest(req, res);
7793
- return true;
7794
- }
7795
-
7796
- if (segments.length === 1 && segments[0] === 'global-ranking-summary') {
7797
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7798
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7799
- return true;
7800
- }
7801
- await handleGlobalRankingSummaryRequest(req, res);
7802
- return true;
7803
- }
7804
-
7805
- if (segments.length === 1 && segments[0] === 'readme-summary') {
7806
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7807
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7808
- return true;
7809
- }
7810
- await handleReadmeSummaryRequest(req, res);
7811
- return true;
7812
- }
7813
-
7814
- if (segments.length === 1 && segments[0] === 'readme-markdown') {
7815
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7816
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7817
- return true;
7818
- }
7819
- await handleReadmeMarkdownRequest(req, res);
7820
- return true;
7821
- }
7822
-
7823
- if (segments.length === 1 && segments[0] === 'support') {
7824
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7825
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7826
- return true;
7827
- }
7828
- await handleSupportInfoRequest(req, res);
7829
- return true;
7830
- }
7831
-
7832
- if (segments.length === 1 && segments[0] === 'bot-contact') {
7833
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7834
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7835
- return true;
7836
- }
7837
- await handleBotContactInfoRequest(req, res);
7838
- return true;
7839
- }
7840
-
7841
- if (segments[0] === 'admin') {
7842
- if (segments.length === 2 && segments[1] === 'overview') {
7843
- await handleAdminOverviewRequest(req, res);
7844
- return true;
7845
- }
7846
- if (segments.length === 2 && segments[1] === 'users') {
7847
- await handleAdminUsersRequest(req, res, url);
7848
- return true;
7849
- }
7850
- if (segments.length === 2 && segments[1] === 'moderators') {
7851
- await handleAdminModeratorsRequest(req, res);
7852
- return true;
7853
- }
7854
- if (segments.length === 3 && segments[1] === 'moderators') {
7855
- await handleAdminModeratorDeleteRequest(req, res, segments[2]);
7856
- return true;
7857
- }
7858
- if (segments.length === 2 && segments[1] === 'packs') {
7859
- await handleAdminPacksRequest(req, res, url);
7860
- return true;
7861
- }
7862
- if (segments.length === 3 && segments[1] === 'packs') {
7863
- await handleAdminPackDetailsRequest(req, res, segments[2]);
7864
- return true;
7865
- }
7866
- if (segments.length === 4 && segments[1] === 'packs' && segments[3] === 'delete') {
7867
- await handleAdminPackDeleteRequest(req, res, segments[2]);
7868
- return true;
7869
- }
7870
- if (segments.length === 6 && segments[1] === 'packs' && segments[3] === 'stickers' && segments[5] === 'delete') {
7871
- await handleAdminPackStickerDeleteRequest(req, res, segments[2], segments[4]);
7872
- return true;
7873
- }
7874
- if (segments.length === 4 && segments[1] === 'stickers' && segments[3] === 'delete') {
7875
- await handleAdminGlobalStickerDeleteRequest(req, res, segments[2]);
7876
- return true;
7877
- }
7878
- if (segments.length === 2 && segments[1] === 'bans') {
7879
- await handleAdminBansRequest(req, res);
7880
- return true;
7881
- }
7882
- if (segments.length === 4 && segments[1] === 'bans' && segments[3] === 'revoke') {
7883
- await handleAdminBanRevokeRequest(req, res, segments[2]);
7884
- return true;
7885
- }
7886
- sendJson(req, res, 404, { error: 'Rota admin nao encontrada.' });
7887
- return true;
7888
- }
7889
-
7890
- if (segments.length === 1) {
7891
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7892
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7893
- return true;
7894
- }
7895
- await handleDetailsRequest(req, res, segments[0], url);
7896
- return true;
7897
- }
7898
-
7899
- if (segments.length === 2 && ['open', 'like', 'dislike'].includes(segments[1])) {
7900
- if (req.method !== 'POST') {
7901
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7902
- return true;
7903
- }
7904
- await handlePackInteractionRequest(req, res, segments[0], segments[1], url);
7905
- return true;
7906
- }
7907
-
7908
- if (segments.length === 2 && segments[1] === 'manage') {
7909
- await handleManagedPackRequest(req, res, segments[0]);
7910
- return true;
7911
- }
7912
-
7913
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'clone') {
7914
- await handleManagedPackCloneRequest(req, res, segments[0]);
7915
- return true;
7916
- }
7917
-
7918
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'cover') {
7919
- await handleManagedPackCoverRequest(req, res, segments[0]);
7920
- return true;
7921
- }
7922
-
7923
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'reorder') {
7924
- await handleManagedPackReorderRequest(req, res, segments[0]);
7925
- return true;
7926
- }
7927
-
7928
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'analytics') {
7929
- await handleManagedPackAnalyticsRequest(req, res, segments[0]);
7930
- return true;
7931
- }
7932
-
7933
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'stickers') {
7934
- await handleManagedPackStickerCreateRequest(req, res, segments[0]);
7935
- return true;
7936
- }
7937
-
7938
- if (segments.length === 4 && segments[1] === 'manage' && segments[2] === 'stickers') {
7939
- await handleManagedPackStickerDeleteRequest(req, res, segments[0], segments[3]);
7940
- return true;
7941
- }
7942
-
7943
- if (segments.length === 5 && segments[1] === 'manage' && segments[2] === 'stickers' && segments[4] === 'replace') {
7944
- await handleManagedPackStickerReplaceRequest(req, res, segments[0], segments[3]);
7945
- return true;
7946
- }
7947
-
7948
- if (segments.length === 2 && segments[1] === 'publish-state') {
7949
- if (!['GET', 'HEAD', 'POST'].includes(req.method || '')) {
7950
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7951
- return true;
7952
- }
7953
- await handlePackPublishStateRequest(req, res, segments[0], url);
7954
- return true;
7955
- }
7956
-
7957
- if (segments.length === 2 && segments[1] === 'finalize') {
7958
- if (req.method !== 'POST') {
7959
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7960
- return true;
7961
- }
7962
- await handleFinalizePackRequest(req, res, segments[0]);
7963
- return true;
7964
- }
7965
-
7966
- if (segments.length === 2 && segments[1] === 'stickers-upload') {
7967
- if (req.method !== 'POST') {
7968
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7969
- return true;
7970
- }
7971
- await handleUploadStickerToPackRequest(req, res, segments[0]);
7972
- return true;
7973
- }
7974
-
7975
- if (segments.length === 3 && segments[1] === 'stickers') {
7976
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7977
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7978
- return true;
7979
- }
7980
- await handleAssetRequest(req, res, segments[0], segments[2]);
7981
- return true;
7982
- }
7941
+ const catalogApiRouter = createCatalogApiRouter({
7942
+ apiBasePath: STICKER_API_BASE_PATH,
7943
+ orphanApiPath: STICKER_ORPHAN_API_PATH,
7944
+ sendJson,
7945
+ handlers: {
7946
+ handleCreatePackRequest,
7947
+ handleGoogleAuthSessionRequest,
7948
+ handleMyProfileRequest,
7949
+ handleAdminPanelSessionRequest,
7950
+ handleListRequest,
7951
+ handleIntentCollectionsRequest,
7952
+ handleCreatorRankingRequest,
7953
+ handleRecommendationsRequest,
7954
+ handleMarketplaceStatsRequest,
7955
+ handleCreatePackConfigRequest,
7956
+ handleOrphanStickerListRequest,
7957
+ handleDataFileListRequest,
7958
+ handleSystemSummaryRequest,
7959
+ handleGitHubProjectSummaryRequest,
7960
+ handleGlobalRankingSummaryRequest,
7961
+ handleReadmeSummaryRequest,
7962
+ handleReadmeMarkdownRequest,
7963
+ handleSupportInfoRequest,
7964
+ handleBotContactInfoRequest,
7965
+ handleAdminOverviewRequest,
7966
+ handleAdminUsersRequest,
7967
+ handleAdminModeratorsRequest,
7968
+ handleAdminModeratorDeleteRequest,
7969
+ handleAdminPacksRequest,
7970
+ handleAdminPackDetailsRequest,
7971
+ handleAdminPackDeleteRequest,
7972
+ handleAdminPackStickerDeleteRequest,
7973
+ handleAdminGlobalStickerDeleteRequest,
7974
+ handleAdminBansRequest,
7975
+ handleAdminBanRevokeRequest,
7976
+ handleDetailsRequest,
7977
+ handlePackInteractionRequest,
7978
+ handleManagedPackRequest,
7979
+ handleManagedPackCloneRequest,
7980
+ handleManagedPackCoverRequest,
7981
+ handleManagedPackReorderRequest,
7982
+ handleManagedPackAnalyticsRequest,
7983
+ handleManagedPackStickerCreateRequest,
7984
+ handleManagedPackStickerDeleteRequest,
7985
+ handleManagedPackStickerReplaceRequest,
7986
+ handlePackPublishStateRequest,
7987
+ handleFinalizePackRequest,
7988
+ handleUploadStickerToPackRequest,
7989
+ handleAssetRequest,
7990
+ },
7991
+ });
7983
7992
 
7984
- sendJson(req, res, 404, { error: 'Rota de sticker pack nao encontrada.' });
7985
- return true;
7986
- };
7993
+ const handleCatalogApiRequest = async (req, res, pathname, url) =>
7994
+ catalogApiRouter({ req, res, pathname, url });
7987
7995
 
7988
7996
  const handleCatalogPageRequest = async (req, res, pathname) => {
7989
7997
  const normalizedPath = pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;