@kaikybrofc/omnizap-system 2.2.3 → 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 (55) hide show
  1. package/.env.example +13 -0
  2. package/README.md +29 -85
  3. package/app/controllers/messageController.js +133 -1
  4. package/app/modules/stickerPackModule/catalogHandlers/catalogAdminHttp.js +68 -0
  5. package/app/modules/stickerPackModule/catalogHandlers/catalogAuthHttp.js +34 -0
  6. package/app/modules/stickerPackModule/catalogHandlers/catalogPublicHttp.js +179 -0
  7. package/app/modules/stickerPackModule/catalogHandlers/catalogUploadHttp.js +92 -0
  8. package/app/modules/stickerPackModule/catalogRouter.js +79 -0
  9. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +243 -0
  10. package/app/modules/stickerPackModule/domainEvents.js +61 -0
  11. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +21 -0
  12. package/app/modules/stickerPackModule/stickerAssetRepository.js +19 -0
  13. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +55 -15
  14. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +238 -0
  15. package/app/modules/stickerPackModule/stickerDomainEventBus.js +71 -0
  16. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +198 -0
  17. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  18. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +1090 -659
  19. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +19 -1
  20. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +44 -0
  21. package/app/modules/stickerPackModule/stickerPackItemRepository.js +18 -0
  22. package/app/modules/stickerPackModule/stickerPackRepository.js +51 -0
  23. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +191 -0
  24. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +301 -0
  25. package/app/modules/stickerPackModule/stickerStorageService.js +111 -10
  26. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +21 -0
  27. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +59 -7
  28. package/app/observability/metrics.js +169 -0
  29. package/app/services/featureFlagService.js +137 -0
  30. package/app/services/lidMapService.js +4 -1
  31. package/app/services/whatsappLoginLinkService.js +232 -0
  32. package/database/index.js +5 -0
  33. package/database/migrations/20260228_0021_sticker_web_google_owner_phone.sql +83 -0
  34. package/database/migrations/20260228_0022_sticker_scale_indexes.sql +16 -0
  35. package/database/migrations/20260228_0023_sticker_pack_score_snapshot.sql +25 -0
  36. package/database/migrations/20260228_0024_domain_event_outbox.sql +42 -0
  37. package/database/migrations/20260228_0025_sticker_worker_task_idempotency_dlq.sql +23 -0
  38. package/database/migrations/20260228_0026_feature_flags.sql +21 -0
  39. package/ecosystem.prod.config.cjs +70 -9
  40. package/index.js +26 -0
  41. package/package.json +5 -1
  42. package/public/index.html +128 -10
  43. package/public/js/apps/createPackApp.js +59 -272
  44. package/public/js/apps/homeApp.js +106 -0
  45. package/public/js/apps/loginApp.js +459 -0
  46. package/public/js/apps/stickersApp.js +34 -37
  47. package/public/js/apps/userApp.js +244 -0
  48. package/public/js/runtime/react-runtime.js +1 -0
  49. package/public/login/index.html +333 -0
  50. package/public/stickers/create/index.html +2 -1
  51. package/public/stickers/index.html +2 -1
  52. package/public/user/index.html +367 -0
  53. package/scripts/cache-bust.mjs +65 -11
  54. package/scripts/sticker-catalog-loadtest.mjs +208 -0
  55. 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';
@@ -8,6 +9,7 @@ import { getJidUser, normalizeJid, resolveBotJid } from '../../config/baileysCon
8
9
  import { getAdminPhone, getAdminRawValue, resolveAdminJid } from '../../config/adminIdentity.js';
9
10
  import { getActiveSocket } from '../../services/socketState.js';
10
11
  import { extractUserIdInfo, resolveUserId } from '../../services/lidMapService.js';
12
+ import { resolveWhatsAppOwnerJidFromLoginPayload, toWhatsAppOwnerJid, toWhatsAppPhoneDigits } from '../../services/whatsappLoginLinkService.js';
11
13
  import logger from '../../utils/logger/loggerModule.js';
12
14
  import { getSystemMetrics } from '../../utils/systemMetrics/systemMetricsModule.js';
13
15
  import {
@@ -63,6 +65,8 @@ import {
63
65
  buildViewerTagAffinity,
64
66
  computePackSignals,
65
67
  } from './stickerPackMarketplaceService.js';
68
+ import { listStickerPackScoreSnapshotsByPackIds } from './stickerPackScoreSnapshotRepository.js';
69
+ import { createCatalogApiRouter } from './catalogRouter.js';
66
70
  import {
67
71
  buildAdminMenu,
68
72
  buildAiMenu,
@@ -74,11 +78,17 @@ import {
74
78
  buildStickerMenu,
75
79
  } from '../menuModule/common.js';
76
80
  import { getMarketplaceDriftSnapshot } from './stickerMarketplaceDriftService.js';
77
- import { getStickerStorageConfig, readStickerAssetBuffer, saveStickerAssetFromBuffer } from './stickerStorageService.js';
81
+ import {
82
+ getStickerAssetExternalUrl,
83
+ getStickerStorageConfig,
84
+ readStickerAssetBuffer,
85
+ saveStickerAssetFromBuffer,
86
+ } from './stickerStorageService.js';
78
87
  import { convertToWebp } from '../stickerModule/convertToWebp.js';
79
88
  import { sanitizeText } from './stickerPackUtils.js';
80
89
  import stickerPackService from './stickerPackServiceRuntime.js';
81
90
  import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
91
+ import { isFeatureEnabled } from '../../services/featureFlagService.js';
82
92
 
83
93
  const parseEnvBool = (value, fallback) => {
84
94
  if (value === undefined || value === null || value === '') return fallback;
@@ -143,12 +153,15 @@ const STICKER_API_BASE_PATH = normalizeBasePath(process.env.STICKER_API_BASE_PAT
143
153
  const STICKER_ORPHAN_API_PATH = `${STICKER_API_BASE_PATH}/orphan-stickers`;
144
154
  const STICKER_CREATE_WEB_PATH = `${STICKER_WEB_PATH}/create`;
145
155
  const STICKER_ADMIN_WEB_PATH = `${STICKER_WEB_PATH}/admin`;
156
+ const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_PATH, '/login');
157
+ const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PATH, '/user');
146
158
  const STICKER_DATA_PUBLIC_PATH = normalizeBasePath(process.env.STICKER_DATA_PUBLIC_PATH, '/data');
147
159
  const STICKER_DATA_PUBLIC_DIR = path.resolve(process.env.STICKER_DATA_PUBLIC_DIR || path.join(process.cwd(), 'data'));
148
160
  const CATALOG_PUBLIC_DIR = path.resolve(process.cwd(), 'public');
149
161
  const CATALOG_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'index.html');
150
162
  const CREATE_PACK_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'create', 'index.html');
151
163
  const ADMIN_PANEL_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'admin', 'index.html');
164
+ const USER_DASHBOARD_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'user', 'index.html');
152
165
  const CATALOG_STYLES_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'css', 'styles.css');
153
166
  const CATALOG_SCRIPT_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'js', 'catalog.js');
154
167
  const DEFAULT_LIST_LIMIT = clampInt(process.env.STICKER_WEB_LIST_LIMIT, 24, 1, 60);
@@ -199,6 +212,19 @@ const GITHUB_REPOSITORY = String(process.env.GITHUB_REPOSITORY || 'Kaikygr/omniz
199
212
  const GITHUB_TOKEN = String(process.env.GITHUB_TOKEN || '').trim();
200
213
  const GITHUB_PROJECT_CACHE_SECONDS = clampInt(process.env.GITHUB_PROJECT_CACHE_SECONDS, 300, 30, 3600);
201
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
+ );
202
228
  const MARKETPLACE_GLOBAL_STATS_API_PATH = '/api/marketplace/stats';
203
229
  const MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS = clampInt(process.env.MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS, 45, 30, 60);
204
230
  const HOME_MARKETPLACE_STATS_CACHE_SECONDS = clampInt(process.env.HOME_MARKETPLACE_STATS_CACHE_SECONDS, 45, 10, 300);
@@ -214,6 +240,14 @@ const SITE_CANONICAL_REDIRECT_ENABLED = parseEnvBool(process.env.SITE_CANONICAL_
214
240
  const SITE_ORIGIN = String(process.env.SITE_ORIGIN || `${SITE_CANONICAL_SCHEME}://${SITE_CANONICAL_HOST}`)
215
241
  .trim()
216
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(/\.+$/, '');
217
251
  const SITEMAP_MAX_PACKS = clampInt(process.env.STICKER_SITEMAP_MAX_PACKS, 45000, 100, 50000);
218
252
  const SITEMAP_CACHE_SECONDS = clampInt(process.env.STICKER_SITEMAP_CACHE_SECONDS, 180, 30, 3600);
219
253
  const SEO_DISCOVERY_LINK_LIMIT = clampInt(process.env.STICKER_SEO_DISCOVERY_LINK_LIMIT, 60, 10, 200);
@@ -291,6 +325,9 @@ const MARKETPLACE_GLOBAL_STATS_CACHE = {
291
325
  pending: null,
292
326
  };
293
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();
294
331
  const SYSTEM_SUMMARY_CACHE = {
295
332
  expiresAt: 0,
296
333
  value: null,
@@ -320,6 +357,69 @@ const formatDuration = (totalSeconds) => {
320
357
  return days > 0 ? `${days}d ${hhmmss}` : hhmmss;
321
358
  };
322
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
+
323
423
  const sendJson = (req, res, statusCode, payload) => {
324
424
  const body = JSON.stringify(payload);
325
425
  res.statusCode = statusCode;
@@ -358,6 +458,22 @@ const toRequestHost = (req) =>
358
458
  .replace(/\.$/, '')
359
459
  .split(':')[0];
360
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
+
361
477
  const maybeRedirectToCanonicalHost = (req, res, url) => {
362
478
  if (!SITE_CANONICAL_REDIRECT_ENABLED) return false;
363
479
  if (!['GET', 'HEAD'].includes(req.method || '')) return false;
@@ -391,6 +507,33 @@ const parseCookies = (req) => {
391
507
  }, {});
392
508
  };
393
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
+
394
537
  const isRequestSecure = (req) => {
395
538
  const proto = String(req?.headers?.['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase();
396
539
  if (proto) return proto === 'https';
@@ -499,6 +642,11 @@ const runSqlTransaction = async (handler) => {
499
642
  const buildCookieString = (name, value, req, options = {}) => {
500
643
  const parts = [`${name}=${encodeURIComponent(String(value ?? ''))}`];
501
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}`);
502
650
  if (options.httpOnly !== false) parts.push('HttpOnly');
503
651
  parts.push(`SameSite=${options.sameSite || 'Lax'}`);
504
652
  if (isRequestSecure(req)) parts.push('Secure');
@@ -910,16 +1058,22 @@ const pruneExpiredGoogleSessions = () => {
910
1058
  }
911
1059
  };
912
1060
 
913
- const getGoogleWebSessionTokenFromRequest = (req) => {
1061
+ const getGoogleWebSessionTokensFromRequest = (req) => {
1062
+ const direct = getCookieValuesFromRequest(req, GOOGLE_WEB_SESSION_COOKIE_NAME);
1063
+ if (direct.length > 0) return direct;
914
1064
  const cookies = parseCookies(req);
915
- return String(cookies[GOOGLE_WEB_SESSION_COOKIE_NAME] || '').trim();
1065
+ const fallback = String(cookies[GOOGLE_WEB_SESSION_COOKIE_NAME] || '').trim();
1066
+ return fallback ? [fallback] : [];
916
1067
  };
917
1068
 
1069
+ const getGoogleWebSessionTokenFromRequest = (req) => getGoogleWebSessionTokensFromRequest(req)[0] || '';
1070
+
918
1071
  const normalizeGoogleWebSessionRow = (row) => {
919
1072
  if (!row || typeof row !== 'object') return null;
920
1073
  const token = String(row.session_token || '').trim();
921
1074
  const sub = normalizeGoogleSubject(row.google_sub);
922
1075
  const ownerJid = normalizeJid(row.owner_jid) || '';
1076
+ const ownerPhone = toWhatsAppPhoneDigits(row.owner_phone || ownerJid) || '';
923
1077
  const expiresAt = Number(new Date(row.expires_at || 0));
924
1078
  if (!token || !sub || !ownerJid || !Number.isFinite(expiresAt)) return null;
925
1079
  const createdAtRaw = Number(new Date(row.created_at || 0));
@@ -931,6 +1085,7 @@ const normalizeGoogleWebSessionRow = (row) => {
931
1085
  name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
932
1086
  picture: String(row.picture_url || '').trim() || null,
933
1087
  ownerJid,
1088
+ ownerPhone,
934
1089
  createdAt: Number.isFinite(createdAtRaw) ? createdAtRaw : Date.now(),
935
1090
  expiresAt,
936
1091
  lastSeenAt: Number.isFinite(lastSeenAtRaw) ? lastSeenAtRaw : 0,
@@ -959,10 +1114,20 @@ const upsertGoogleWebUserRecord = async (user, connection = null) => {
959
1114
  const sub = normalizeGoogleSubject(user?.sub);
960
1115
  const ownerJid = normalizeJid(user?.ownerJid) || '';
961
1116
  if (!sub || !ownerJid) return;
1117
+ const ownerPhone = toWhatsAppPhoneDigits(ownerJid) || null;
962
1118
  const email = String(user?.email || '').trim().toLowerCase() || null;
963
1119
  const name = sanitizeText(user?.name || '', 120, { allowEmpty: true }) || null;
964
1120
  const pictureUrl = String(user?.picture || '').trim().slice(0, 1024) || null;
965
1121
 
1122
+ // owner_jid e unico; removemos vinculos antigos desse numero antes do upsert para manter 1:1.
1123
+ await executeQuery(
1124
+ `DELETE FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
1125
+ WHERE owner_jid = ?
1126
+ AND google_sub <> ?`,
1127
+ [ownerJid, sub],
1128
+ connection,
1129
+ );
1130
+
966
1131
  await executeQuery(
967
1132
  `INSERT INTO ${TABLES.STICKER_WEB_GOOGLE_USER}
968
1133
  (google_sub, owner_jid, email, name, picture_url, last_login_at, last_seen_at)
@@ -977,12 +1142,22 @@ const upsertGoogleWebUserRecord = async (user, connection = null) => {
977
1142
  [sub, ownerJid, email, name, pictureUrl],
978
1143
  connection,
979
1144
  );
1145
+
1146
+ // Persistimos o telefone normalizado do owner para facilitar consultas administrativas.
1147
+ await executeQuery(
1148
+ `UPDATE ${TABLES.STICKER_WEB_GOOGLE_USER}
1149
+ SET owner_phone = COALESCE(?, owner_phone)
1150
+ WHERE google_sub = ?`,
1151
+ [ownerPhone, sub],
1152
+ connection,
1153
+ ).catch(() => {});
980
1154
  };
981
1155
 
982
1156
  const upsertGoogleWebSessionRecord = async (session, connection = null) => {
983
1157
  const token = String(session?.token || '').trim();
984
1158
  const sub = normalizeGoogleSubject(session?.sub);
985
1159
  const ownerJid = normalizeJid(session?.ownerJid) || '';
1160
+ const ownerPhone = toWhatsAppPhoneDigits(session?.ownerPhone || ownerJid) || null;
986
1161
  const expiresAt = Number(session?.expiresAt || 0);
987
1162
  if (!token || !sub || !ownerJid || !Number.isFinite(expiresAt) || expiresAt <= 0) return;
988
1163
  const email = String(session?.email || '').trim().toLowerCase() || null;
@@ -1005,6 +1180,42 @@ const upsertGoogleWebSessionRecord = async (session, connection = null) => {
1005
1180
  [token, sub, ownerJid, email, name, pictureUrl, new Date(expiresAt)],
1006
1181
  connection,
1007
1182
  );
1183
+
1184
+ await executeQuery(
1185
+ `UPDATE ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1186
+ SET owner_phone = COALESCE(?, owner_phone)
1187
+ WHERE session_token = ?`,
1188
+ [ownerPhone, token],
1189
+ connection,
1190
+ ).catch(() => {});
1191
+ };
1192
+
1193
+ const deleteOtherGoogleWebSessionsInDb = async ({ token = '', ownerJid = '', sub = '' } = {}, connection = null) => {
1194
+ const sessionToken = String(token || '').trim();
1195
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
1196
+ const normalizedSub = normalizeGoogleSubject(sub);
1197
+ if (!sessionToken || (!normalizedOwnerJid && !normalizedSub)) return 0;
1198
+
1199
+ const clauses = [];
1200
+ const params = [];
1201
+ if (normalizedOwnerJid) {
1202
+ clauses.push('owner_jid = ?');
1203
+ params.push(normalizedOwnerJid);
1204
+ }
1205
+ if (normalizedSub) {
1206
+ clauses.push('google_sub = ?');
1207
+ params.push(normalizedSub);
1208
+ }
1209
+ if (!clauses.length) return 0;
1210
+
1211
+ const result = await executeQuery(
1212
+ `DELETE FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1213
+ WHERE session_token <> ?
1214
+ AND (${clauses.join(' OR ')})`,
1215
+ [sessionToken, ...params],
1216
+ connection,
1217
+ );
1218
+ return Number(result?.affectedRows || 0);
1008
1219
  };
1009
1220
 
1010
1221
  const persistGoogleWebSessionToDb = async (session) => {
@@ -1021,6 +1232,14 @@ const persistGoogleWebSessionToDb = async (session) => {
1021
1232
  },
1022
1233
  connection,
1023
1234
  );
1235
+ await deleteOtherGoogleWebSessionsInDb(
1236
+ {
1237
+ token: session.token,
1238
+ ownerJid: session.ownerJid,
1239
+ sub: session.sub,
1240
+ },
1241
+ connection,
1242
+ );
1024
1243
  await upsertGoogleWebSessionRecord(session, connection);
1025
1244
  });
1026
1245
  };
@@ -1030,7 +1249,7 @@ const findGoogleWebSessionInDbByToken = async (sessionToken) => {
1030
1249
  if (!token) return null;
1031
1250
  await maybePruneExpiredGoogleSessionsFromDb();
1032
1251
  const rows = await executeQuery(
1033
- `SELECT session_token, google_sub, owner_jid, email, name, picture_url, created_at, expires_at, last_seen_at
1252
+ `SELECT session_token, google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, expires_at, last_seen_at
1034
1253
  FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1035
1254
  WHERE session_token = ?
1036
1255
  AND revoked_at IS NULL
@@ -1504,6 +1723,8 @@ const pruneExpiredAdminPanelSessions = () => {
1504
1723
  };
1505
1724
 
1506
1725
  const getAdminPanelSessionTokenFromRequest = (req) => {
1726
+ const direct = getCookieValuesFromRequest(req, ADMIN_PANEL_SESSION_COOKIE_NAME);
1727
+ if (direct.length > 0) return direct[0];
1507
1728
  const cookies = parseCookies(req);
1508
1729
  return String(cookies[ADMIN_PANEL_SESSION_COOKIE_NAME] || '').trim();
1509
1730
  };
@@ -1515,6 +1736,14 @@ const clearAdminPanelSessionCookie = (req, res) => {
1515
1736
  maxAgeSeconds: 0,
1516
1737
  }),
1517
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
+ );
1518
1747
  };
1519
1748
 
1520
1749
  const createAdminPanelSession = (googleSession, { role = 'owner' } = {}) => {
@@ -1624,83 +1853,105 @@ const requireOwnerAdminPanelSession = (req, res) => {
1624
1853
  return session;
1625
1854
  };
1626
1855
 
1627
- const createGoogleWebSession = (claims) => {
1856
+ const createGoogleWebSession = (claims, { ownerJid } = {}) => {
1628
1857
  pruneExpiredGoogleSessions();
1629
1858
  const token = randomUUID();
1630
1859
  const now = Date.now();
1860
+ const resolvedOwnerJid = normalizeJid(ownerJid) || buildGoogleOwnerJid(claims.sub);
1861
+ const resolvedOwnerPhone = toWhatsAppPhoneDigits(resolvedOwnerJid) || '';
1631
1862
  const session = {
1632
1863
  token,
1633
1864
  sub: claims.sub,
1634
1865
  email: claims.email || null,
1635
1866
  name: claims.name || null,
1636
1867
  picture: claims.picture || null,
1637
- ownerJid: buildGoogleOwnerJid(claims.sub),
1868
+ ownerJid: resolvedOwnerJid,
1869
+ ownerPhone: resolvedOwnerPhone,
1638
1870
  createdAt: now,
1639
1871
  expiresAt: now + STICKER_WEB_GOOGLE_SESSION_TTL_MS,
1640
1872
  lastSeenAt: now,
1641
1873
  lastDbTouchAt: 0,
1642
1874
  };
1643
- webGoogleSessionMap.set(token, session);
1644
1875
  return session;
1645
1876
  };
1646
1877
 
1878
+ const activateGoogleWebSession = (session) => {
1879
+ if (!session?.token) return;
1880
+ pruneExpiredGoogleSessions();
1881
+ webGoogleSessionMap.set(session.token, session);
1882
+ for (const [token, existing] of webGoogleSessionMap.entries()) {
1883
+ if (!existing || token === session.token) continue;
1884
+ const sameOwner = normalizeJid(existing.ownerJid) === normalizeJid(session.ownerJid);
1885
+ const sameSub = normalizeGoogleSubject(existing.sub) === normalizeGoogleSubject(session.sub);
1886
+ if (sameOwner || sameSub) {
1887
+ webGoogleSessionMap.delete(token);
1888
+ }
1889
+ }
1890
+ };
1891
+
1647
1892
  const resolveGoogleWebSessionFromRequest = async (req) => {
1648
1893
  pruneExpiredGoogleSessions();
1649
- const sessionToken = getGoogleWebSessionTokenFromRequest(req);
1650
- if (!sessionToken) return null;
1651
- const session = webGoogleSessionMap.get(sessionToken);
1652
- 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;
1653
1900
  if (Number(session.expiresAt || 0) <= Date.now()) {
1654
1901
  webGoogleSessionMap.delete(sessionToken);
1655
- } else {
1656
- const now = Date.now();
1657
- session.lastSeenAt = now;
1658
- if (now - Number(session.lastDbTouchAt || 0) >= GOOGLE_WEB_SESSION_DB_TOUCH_INTERVAL_MS) {
1659
- session.lastDbTouchAt = now;
1660
- void touchGoogleWebSessionSeenInDb(sessionToken).catch((error) => {
1661
- logger.warn('Falha ao atualizar last_seen da sessão Google web.', {
1662
- action: 'sticker_pack_google_web_session_touch_failed',
1663
- error: error?.message,
1664
- });
1665
- });
1666
- void touchGoogleWebUserSeenInDb(session.sub).catch(() => {});
1667
- }
1668
- try {
1669
- await assertGoogleIdentityNotBanned({
1670
- sub: session.sub,
1671
- email: session.email,
1672
- 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,
1673
1913
  });
1674
- } catch (banError) {
1675
- webGoogleSessionMap.delete(sessionToken);
1676
- void deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1677
- return null;
1678
- }
1679
- return session;
1914
+ });
1915
+ void touchGoogleWebUserSeenInDb(session.sub).catch(() => {});
1680
1916
  }
1681
- }
1682
- try {
1683
- const persistedSession = await findGoogleWebSessionInDbByToken(sessionToken);
1684
- if (!persistedSession) return null;
1685
1917
  try {
1686
1918
  await assertGoogleIdentityNotBanned({
1687
- sub: persistedSession.sub,
1688
- email: persistedSession.email,
1689
- ownerJid: persistedSession.ownerJid,
1919
+ sub: session.sub,
1920
+ email: session.email,
1921
+ ownerJid: session.ownerJid,
1690
1922
  });
1923
+ return session;
1691
1924
  } catch {
1692
- await deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1693
- return null;
1925
+ webGoogleSessionMap.delete(sessionToken);
1926
+ void deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1927
+ }
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
+ });
1694
1951
  }
1695
- webGoogleSessionMap.set(sessionToken, persistedSession);
1696
- return persistedSession;
1697
- } catch (error) {
1698
- logger.warn('Falha ao resolver sessão Google web no banco.', {
1699
- action: 'sticker_pack_google_web_session_db_resolve_failed',
1700
- error: error?.message,
1701
- });
1702
- return null;
1703
1952
  }
1953
+
1954
+ return null;
1704
1955
  };
1705
1956
 
1706
1957
  const clearGoogleWebSessionCookie = (req, res) => {
@@ -1710,6 +1961,14 @@ const clearGoogleWebSessionCookie = (req, res) => {
1710
1961
  maxAgeSeconds: 0,
1711
1962
  }),
1712
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
+ );
1713
1972
  };
1714
1973
 
1715
1974
  const sendAsset = (req, res, buffer, mimetype = 'image/webp') => {
@@ -2060,7 +2319,9 @@ const resolveWebCreateOwnerJid = async (explicitOwner = '') => {
2060
2319
  const resolvedAdminJid = await resolveAdminJid();
2061
2320
  const fromAdmin = toOwnerJid(resolvedAdminJid);
2062
2321
  if (fromAdmin) return fromAdmin;
2063
- } catch {}
2322
+ } catch {
2323
+ // Ignore fallback errors while resolving owner identity.
2324
+ }
2064
2325
 
2065
2326
  const adminCandidates = [
2066
2327
  getAdminRawValue(),
@@ -2230,14 +2491,18 @@ const resolveSupportAdminPhone = async () => {
2230
2491
  const resolvedFromLidMap = await resolveUserId(extractUserIdInfo(adminRaw));
2231
2492
  const resolvedPhoneFromLidMap = isPlausibleWhatsAppPhone(getJidUser(resolvedFromLidMap || ''));
2232
2493
  if (resolvedPhoneFromLidMap) return resolvedPhoneFromLidMap;
2233
- } catch {}
2494
+ } catch {
2495
+ // Ignore and fallback to other admin sources.
2496
+ }
2234
2497
  }
2235
2498
 
2236
2499
  try {
2237
2500
  const resolvedAdminJid = await resolveAdminJid();
2238
2501
  const resolvedPhone = isPlausibleWhatsAppPhone(getJidUser(resolvedAdminJid || ''));
2239
2502
  if (resolvedPhone) return resolvedPhone;
2240
- } catch {}
2503
+ } catch {
2504
+ // Ignore and fallback to static admin phone sources.
2505
+ }
2241
2506
 
2242
2507
  const rawPhone = isPlausibleWhatsAppPhone(getJidUser(adminRaw) || adminRaw);
2243
2508
  if (rawPhone) return rawPhone;
@@ -2266,6 +2531,27 @@ const buildSupportInfo = async () => {
2266
2531
  };
2267
2532
  };
2268
2533
 
2534
+ const buildBotContactInfo = () => {
2535
+ const phone = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '');
2536
+ if (!phone) return null;
2537
+ const loginText = String(process.env.WHATSAPP_LOGIN_TRIGGER || 'iniciar').trim() || 'iniciar';
2538
+ const menuText = `${PACK_COMMAND_PREFIX}menu`;
2539
+ const buildUrl = (text) =>
2540
+ `https://api.whatsapp.com/send/?phone=${encodeURIComponent(phone)}&text=${encodeURIComponent(
2541
+ String(text || '').trim(),
2542
+ )}&type=custom_url&app_absent=0`;
2543
+
2544
+ return {
2545
+ phone,
2546
+ login_text: loginText,
2547
+ menu_text: menuText,
2548
+ urls: {
2549
+ login: buildUrl(loginText),
2550
+ menu: buildUrl(menuText),
2551
+ },
2552
+ };
2553
+ };
2554
+
2269
2555
  const listDataImageFiles = async () => {
2270
2556
  const files = [];
2271
2557
  const queue = [STICKER_DATA_PUBLIC_DIR];
@@ -2618,6 +2904,10 @@ const hydrateMarketplaceEntries = async (packs, { includeItems = true, driftSnap
2618
2904
  const packIds = packs.map((pack) => pack.id);
2619
2905
  const engagementByPackId = await listStickerPackEngagementByPackIds(packIds);
2620
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();
2621
2911
 
2622
2912
  const entries = [];
2623
2913
  const packClassificationById = new Map();
@@ -2636,14 +2926,24 @@ const hydrateMarketplaceEntries = async (packs, { includeItems = true, driftSnap
2636
2926
  const packMetadata = parsePackDescriptionMetadata(pack.description);
2637
2927
  const decoratedClassification = decoratePackClassificationSummary(packClassification);
2638
2928
  const mergedPackTags = mergeUniqueTags(decoratedClassification?.tags || [], packMetadata.tags);
2639
- const signals = computePackSignals({
2640
- pack: { ...pack, items },
2641
- engagement,
2642
- packClassification,
2643
- itemClassifications: orderedClassifications,
2644
- interactionStats,
2645
- scoringWeights: driftSnapshot?.weights || null,
2646
- });
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
+ });
2647
2947
 
2648
2948
  const entry = {
2649
2949
  pack,
@@ -2869,6 +3169,7 @@ const renderCatalogHtml = async ({ initialPackKey }) => {
2869
3169
  __STICKER_WEB_PATH__: escapeHtmlAttribute(STICKER_WEB_PATH),
2870
3170
  __STICKER_API_BASE_PATH__: escapeHtmlAttribute(STICKER_API_BASE_PATH),
2871
3171
  __STICKER_ORPHAN_API_PATH__: escapeHtmlAttribute(buildOrphanStickersApiUrl()),
3172
+ __STICKER_LOGIN_WEB_PATH__: escapeHtmlAttribute(STICKER_LOGIN_WEB_PATH),
2872
3173
  __STICKER_DATA_PUBLIC_PATH__: escapeHtmlAttribute(STICKER_DATA_PUBLIC_PATH),
2873
3174
  __DEFAULT_LIST_LIMIT__: String(DEFAULT_LIST_LIMIT),
2874
3175
  __DEFAULT_ORPHAN_LIST_LIMIT__: String(DEFAULT_ORPHAN_LIST_LIMIT),
@@ -3014,11 +3315,12 @@ const renderPackSeoHtml = ({ packSummary }) => {
3014
3315
  data-web-path="${escapeHtmlAttribute(STICKER_WEB_PATH)}"
3015
3316
  data-api-base-path="${escapeHtmlAttribute(STICKER_API_BASE_PATH)}"
3016
3317
  data-orphan-api-path="${escapeHtmlAttribute(STICKER_ORPHAN_API_PATH)}"
3318
+ data-login-path="${escapeHtmlAttribute(STICKER_LOGIN_WEB_PATH)}"
3017
3319
  data-default-limit="${DEFAULT_LIST_LIMIT}"
3018
3320
  data-default-orphan-limit="${DEFAULT_ORPHAN_LIST_LIMIT}"
3019
3321
  data-initial-pack-key="${escapeHtmlAttribute(packSummary?.pack_key || '')}"
3020
3322
  ></div>
3021
- <script type="module" src="/js/apps/stickersApp.js?v=20260227-ui-hotfix-v5"></script>
3323
+ <script type="module" src="/js/apps/stickersApp.js?v=20260228-login-redirect-my-packs1"></script>
3022
3324
  </body>
3023
3325
  </html>`;
3024
3326
  };
@@ -3054,6 +3356,7 @@ const renderCreatePackHtml = async () => {
3054
3356
  const replacements = {
3055
3357
  __STICKER_WEB_PATH__: escapeHtmlAttribute(STICKER_WEB_PATH),
3056
3358
  __STICKER_CREATE_WEB_PATH__: escapeHtmlAttribute(STICKER_CREATE_WEB_PATH),
3359
+ __STICKER_LOGIN_WEB_PATH__: escapeHtmlAttribute(STICKER_LOGIN_WEB_PATH),
3057
3360
  __STICKER_API_BASE_PATH__: escapeHtmlAttribute(STICKER_API_BASE_PATH),
3058
3361
  __PACK_COMMAND_PREFIX__: escapeHtmlAttribute(PACK_COMMAND_PREFIX),
3059
3362
  __CURRENT_YEAR__: String(new Date().getFullYear()),
@@ -3082,6 +3385,23 @@ const renderAdminPanelHtml = async () => {
3082
3385
  return html;
3083
3386
  };
3084
3387
 
3388
+ const renderUserDashboardHtml = async () => {
3389
+ const template = await fs.readFile(USER_DASHBOARD_TEMPLATE_PATH, 'utf8');
3390
+ const replacements = {
3391
+ __STICKER_WEB_PATH__: escapeHtmlAttribute(STICKER_WEB_PATH),
3392
+ __STICKER_LOGIN_WEB_PATH__: escapeHtmlAttribute(STICKER_LOGIN_WEB_PATH),
3393
+ __STICKER_API_BASE_PATH__: escapeHtmlAttribute(STICKER_API_BASE_PATH),
3394
+ __USER_PROFILE_WEB_PATH__: escapeHtmlAttribute(USER_PROFILE_WEB_PATH),
3395
+ __CURRENT_YEAR__: String(new Date().getFullYear()),
3396
+ };
3397
+
3398
+ let html = template;
3399
+ for (const [token, value] of Object.entries(replacements)) {
3400
+ html = html.replaceAll(token, value);
3401
+ }
3402
+ return html;
3403
+ };
3404
+
3085
3405
  const buildSitemapXml = async () => {
3086
3406
  if (SITEMAP_CACHE.expiresAt > Date.now() && SITEMAP_CACHE.xml) {
3087
3407
  return SITEMAP_CACHE.xml;
@@ -3194,95 +3514,118 @@ const handleListRequest = async (req, res, url) => {
3194
3514
  const limit = clampInt(url.searchParams.get('limit'), DEFAULT_LIST_LIMIT, 1, MAX_LIST_LIMIT);
3195
3515
  const offset = clampInt(url.searchParams.get('offset'), 0, 0, 100000);
3196
3516
  const normalizedIntent = normalizeCategoryToken(intent).replace(/-/g, '_');
3197
- const batchLimit = Math.max(limit, Math.min(MAX_LIST_LIMIT, 24));
3198
- const maxPagesToScan = 8;
3199
3517
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
3200
3518
  const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
3201
- const seenPackIds = new Set();
3202
- const collectedEntries = [];
3203
- const driftSnapshot = await getMarketplaceDriftSnapshot();
3204
- let sourceHasMore = true;
3205
- let cursorOffset = offset;
3206
- 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
+ });
3207
3598
 
3208
- while (collectedEntries.length < limit && sourceHasMore && pagesScanned < maxPagesToScan) {
3209
- pagesScanned += 1;
3210
- const { packs, hasMore } = await listStickerPacksForCatalog({
3211
- visibility,
3212
- search: q,
3213
- limit: batchLimit,
3214
- offset: cursorOffset,
3215
- });
3216
- sourceHasMore = hasMore;
3217
- cursorOffset += batchLimit;
3218
- if (!packs.length) break;
3219
-
3220
- const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
3221
- const entriesClassified = STICKER_CATALOG_ONLY_CLASSIFIED
3222
- ? entries.filter((entry) => isPackClassified(entry.packClassification))
3223
- : entries;
3224
- const entriesByCategory = categories.length
3225
- ? entriesClassified.filter((entry) => hasAnyCategory(entry.packClassification?.tags || [], categories))
3226
- : entriesClassified;
3227
- const entriesBySensitivity = includeSensitive
3228
- ? entriesByCategory
3229
- : entriesByCategory.filter((entry) => entry.signals?.nsfw_level === 'safe');
3230
- const entriesByIntent = intent
3231
- ? entriesBySensitivity.filter((entry) => classifyPackIntent(entry) === normalizedIntent)
3232
- : entriesBySensitivity;
3233
- const sortedEntries = [...entriesByIntent].sort((left, right) => {
3234
- const completenessDelta = compareEntriesByPackCompleteness(left, right);
3235
- if (completenessDelta !== 0) return completenessDelta;
3236
- if (sort === 'recent') {
3237
- return Date.parse(right?.pack?.created_at || right?.pack?.updated_at || 0) - Date.parse(left?.pack?.created_at || left?.pack?.updated_at || 0);
3238
- }
3239
- if (sort === 'likes') {
3240
- return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3241
- }
3242
- if (sort === 'downloads') {
3243
- return Number(right?.engagement?.open_count || 0) - Number(left?.engagement?.open_count || 0);
3244
- }
3245
- if (sort === 'comments') {
3246
- const commentDelta = Number(right?.engagement?.comment_count || 0) - Number(left?.engagement?.comment_count || 0);
3247
- if (commentDelta !== 0) return commentDelta;
3248
- return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3249
- }
3250
- if (sort === 'trending') {
3251
- const trendDelta = Number(right?.signals?.trend_score || 0) - Number(left?.signals?.trend_score || 0);
3252
- 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
+ }
3253
3606
  }
3254
- const leftScore = Number(left?.signals?.ranking_score || 0);
3255
- const rightScore = Number(right?.signals?.ranking_score || 0);
3256
- if (rightScore !== leftScore) return rightScore - leftScore;
3257
- return Date.parse(right?.pack?.updated_at || 0) - Date.parse(left?.pack?.updated_at || 0);
3258
- });
3259
-
3260
- for (const entry of sortedEntries) {
3261
- if (!entry?.pack?.id) continue;
3262
- if (seenPackIds.has(entry.pack.id)) continue;
3263
- seenPackIds.add(entry.pack.id);
3264
- collectedEntries.push(entry);
3265
- if (collectedEntries.length >= limit) break;
3266
- }
3267
- }
3268
3607
 
3269
- sendJson(req, res, 200, {
3270
- data: collectedEntries.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
3271
- pagination: {
3272
- limit,
3273
- offset,
3274
- has_more: sourceHasMore,
3275
- next_offset: sourceHasMore ? cursorOffset : null,
3276
- },
3277
- filters: {
3278
- q,
3279
- visibility,
3280
- sort,
3281
- categories,
3282
- intent: intent || null,
3283
- 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
+ };
3284
3625
  },
3285
3626
  });
3627
+
3628
+ sendJson(req, res, 200, payload);
3286
3629
  };
3287
3630
 
3288
3631
  const handleIntentCollectionsRequest = async (req, res, url) => {
@@ -3514,32 +3857,15 @@ const handleGoogleAuthSessionRequest = async (req, res) => {
3514
3857
  if (req.method === 'GET' || req.method === 'HEAD') {
3515
3858
  const session = await resolveGoogleWebSessionFromRequest(req);
3516
3859
  sendJson(req, res, 200, {
3517
- data: session
3518
- ? {
3519
- authenticated: true,
3520
- provider: 'google',
3521
- user: {
3522
- sub: session.sub,
3523
- email: session.email,
3524
- name: session.name,
3525
- picture: session.picture,
3526
- },
3527
- expires_at: toIsoOrNull(session.expiresAt),
3528
- }
3529
- : {
3530
- authenticated: false,
3531
- provider: 'google',
3532
- user: null,
3533
- expires_at: null,
3534
- },
3860
+ data: mapGoogleSessionResponseData(session),
3535
3861
  });
3536
3862
  return;
3537
3863
  }
3538
3864
 
3539
3865
  if (req.method === 'DELETE') {
3540
- const token = getGoogleWebSessionTokenFromRequest(req);
3541
- if (token) webGoogleSessionMap.delete(token);
3542
- if (token) {
3866
+ const tokens = getGoogleWebSessionTokensFromRequest(req);
3867
+ for (const token of tokens) {
3868
+ webGoogleSessionMap.delete(token);
3543
3869
  await deleteGoogleWebSessionFromDb(token).catch((error) => {
3544
3870
  logger.warn('Falha ao remover sessão Google web do banco.', {
3545
3871
  action: 'sticker_pack_google_web_session_db_delete_failed',
@@ -3567,20 +3893,57 @@ const handleGoogleAuthSessionRequest = async (req, res) => {
3567
3893
 
3568
3894
  try {
3569
3895
  const claims = await verifyGoogleIdToken(payload?.google_id_token || payload?.id_token);
3896
+ const linkedOwner = resolveWhatsAppOwnerJidFromLoginPayload(payload);
3897
+ if (!linkedOwner.ownerJid) {
3898
+ if (!linkedOwner.hasPayload) {
3899
+ sendJson(req, res, 400, {
3900
+ error: 'Abra esta pagina pelo link enviado no WhatsApp. Envie "iniciar" no bot para gerar o link de login.',
3901
+ code: 'WHATSAPP_LOGIN_LINK_REQUIRED',
3902
+ reason: 'missing_link',
3903
+ });
3904
+ return;
3905
+ }
3906
+
3907
+ const reason = String(linkedOwner.reason || '').trim().toLowerCase();
3908
+ const isUnauthorizedAttempt = ['invalid_signature', 'missing_signature'].includes(reason);
3909
+ const statusCode = isUnauthorizedAttempt ? 403 : 400;
3910
+ const errorMessage =
3911
+ reason === 'expired'
3912
+ ? 'Link de login expirado. Envie "iniciar" novamente no WhatsApp.'
3913
+ : isUnauthorizedAttempt
3914
+ ? 'Tentativa de login sem permissao detectada. Gere um novo link enviando "iniciar" no privado do bot.'
3915
+ : 'Link de login invalido. Envie "iniciar" novamente no WhatsApp.';
3916
+
3917
+ logger.warn('Tentativa de login web bloqueada por validacao do link WhatsApp.', {
3918
+ action: 'sticker_pack_google_web_login_link_blocked',
3919
+ reason: reason || 'unknown',
3920
+ remote_ip: req.socket?.remoteAddress || null,
3921
+ user_agent: req.headers?.['user-agent'] || null,
3922
+ });
3923
+
3924
+ sendJson(req, res, statusCode, {
3925
+ error: errorMessage,
3926
+ code: 'WHATSAPP_LOGIN_LINK_INVALID',
3927
+ reason: reason || 'invalid_link',
3928
+ });
3929
+ return;
3930
+ }
3931
+ const ownerJid = linkedOwner.ownerJid;
3932
+
3570
3933
  await assertGoogleIdentityNotBanned({
3571
3934
  sub: claims.sub,
3572
3935
  email: claims.email,
3573
- ownerJid: buildGoogleOwnerJid(claims.sub),
3936
+ ownerJid,
3574
3937
  });
3575
- const session = createGoogleWebSession(claims);
3938
+ const session = createGoogleWebSession(claims, { ownerJid });
3576
3939
  if (!session.ownerJid) {
3577
3940
  sendJson(req, res, 400, { error: 'Nao foi possivel vincular a conta Google.' });
3578
3941
  return;
3579
3942
  }
3580
3943
  try {
3581
3944
  await persistGoogleWebSessionToDb(session);
3945
+ activateGoogleWebSession(session);
3582
3946
  } catch (persistError) {
3583
- webGoogleSessionMap.delete(session.token);
3584
3947
  logger.error('Falha ao persistir sessão Google web no banco.', {
3585
3948
  action: 'sticker_pack_google_web_session_db_persist_failed',
3586
3949
  error: persistError?.message,
@@ -3596,17 +3959,7 @@ const handleGoogleAuthSessionRequest = async (req, res) => {
3596
3959
  }),
3597
3960
  );
3598
3961
  sendJson(req, res, 200, {
3599
- data: {
3600
- authenticated: true,
3601
- provider: 'google',
3602
- user: {
3603
- sub: session.sub,
3604
- email: session.email,
3605
- name: session.name,
3606
- picture: session.picture,
3607
- },
3608
- expires_at: toIsoOrNull(session.expiresAt),
3609
- },
3962
+ data: mapGoogleSessionResponseData(session),
3610
3963
  });
3611
3964
  } catch (error) {
3612
3965
  sendJson(req, res, Number(error?.statusCode || 401), {
@@ -3627,20 +3980,199 @@ const mapGoogleSessionResponseData = (session) =>
3627
3980
  name: session.name,
3628
3981
  picture: session.picture,
3629
3982
  },
3983
+ owner_jid: session.ownerJid,
3984
+ owner_phone: toWhatsAppPhoneDigits(session.ownerPhone || session.ownerJid) || null,
3630
3985
  expires_at: toIsoOrNull(session.expiresAt),
3631
3986
  }
3632
3987
  : {
3633
3988
  authenticated: false,
3634
3989
  provider: 'google',
3635
3990
  user: null,
3991
+ owner_jid: null,
3992
+ owner_phone: null,
3636
3993
  expires_at: null,
3637
3994
  };
3638
3995
 
3639
- const handleMyProfileRequest = async (req, res) => {
3640
- if (!['GET', 'HEAD'].includes(req.method || '')) {
3641
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
3642
- return;
3643
- }
3996
+ const buildOwnerLookupJids = (value) => {
3997
+ const normalized = normalizeJid(value) || '';
3998
+ if (!normalized || !normalized.includes('@')) return [];
3999
+ const lookup = new Set([normalized]);
4000
+ const phoneDigits = toWhatsAppPhoneDigits(normalized);
4001
+ if (!phoneDigits) return Array.from(lookup);
4002
+ lookup.add(normalizeJid(`${phoneDigits}@s.whatsapp.net`) || '');
4003
+ lookup.add(normalizeJid(`${phoneDigits}@c.us`) || '');
4004
+ lookup.add(normalizeJid(`${phoneDigits}@hosted`) || '');
4005
+ return Array.from(lookup).filter(Boolean);
4006
+ };
4007
+
4008
+ const appendMyProfileOwnerCandidate = (candidateSet, lookupSet, value) => {
4009
+ const normalized = normalizeJid(value) || '';
4010
+ if (!normalized || !normalized.includes('@')) return;
4011
+
4012
+ candidateSet.add(normalized);
4013
+ for (const lookupJid of buildOwnerLookupJids(normalized)) {
4014
+ lookupSet.add(lookupJid);
4015
+ }
4016
+
4017
+ const phoneOwner = toWhatsAppOwnerJid(value);
4018
+ if (phoneOwner) {
4019
+ candidateSet.add(phoneOwner);
4020
+ for (const lookupJid of buildOwnerLookupJids(phoneOwner)) {
4021
+ lookupSet.add(lookupJid);
4022
+ }
4023
+ }
4024
+ };
4025
+
4026
+ const buildPhoneSet = (...values) => {
4027
+ const set = new Set();
4028
+ for (const value of values) {
4029
+ const digits = toWhatsAppPhoneDigits(value);
4030
+ if (digits) set.add(digits);
4031
+ }
4032
+ return set;
4033
+ };
4034
+
4035
+ const resolveMyProfileOwnerCandidates = async (session) => {
4036
+ const candidates = new Set();
4037
+ const lookupByJid = new Set();
4038
+ const lidCandidates = new Set();
4039
+ const appendCandidate = (value) => appendMyProfileOwnerCandidate(candidates, lookupByJid, value);
4040
+ const trustedPhones = new Set();
4041
+ const blockedJids = new Set();
4042
+ const blockedPhones = new Set();
4043
+
4044
+ appendCandidate(session?.ownerJid);
4045
+ appendCandidate(toWhatsAppOwnerJid(session?.ownerPhone || session?.ownerJid));
4046
+ for (const phone of buildPhoneSet(session?.ownerPhone, session?.ownerJid)) {
4047
+ trustedPhones.add(phone);
4048
+ }
4049
+
4050
+ const activeSocket = getActiveSocket();
4051
+ const botJid = normalizeJid(resolveBotJid(activeSocket?.user?.id || '') || '');
4052
+ if (botJid) {
4053
+ blockedJids.add(botJid);
4054
+ for (const phone of buildPhoneSet(botJid)) {
4055
+ blockedPhones.add(phone);
4056
+ }
4057
+ }
4058
+
4059
+ const legacyGoogleOwner = buildGoogleOwnerJid(session?.sub);
4060
+ if (legacyGoogleOwner) appendCandidate(legacyGoogleOwner);
4061
+
4062
+ const sessionResolved = await resolveUserId(extractUserIdInfo(session?.ownerJid || session?.ownerPhone || null)).catch(() => null);
4063
+ if (sessionResolved) {
4064
+ appendCandidate(sessionResolved);
4065
+ for (const phone of buildPhoneSet(sessionResolved)) {
4066
+ trustedPhones.add(phone);
4067
+ }
4068
+ }
4069
+
4070
+ const normalizedSub = normalizeGoogleSubject(session?.sub);
4071
+ if (normalizedSub) {
4072
+ try {
4073
+ const rows = await executeQuery(
4074
+ `SELECT owner_jid, owner_phone
4075
+ FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
4076
+ WHERE google_sub = ?
4077
+ LIMIT 1`,
4078
+ [normalizedSub],
4079
+ );
4080
+ const row = Array.isArray(rows) ? rows[0] : null;
4081
+ appendCandidate(row?.owner_jid || '');
4082
+ appendCandidate(row?.owner_phone || '');
4083
+ const mappedResolved = await resolveUserId(extractUserIdInfo(row?.owner_jid || row?.owner_phone || null)).catch(() => null);
4084
+ if (mappedResolved) appendCandidate(mappedResolved);
4085
+ } catch (error) {
4086
+ logger.warn('Falha ao resolver owners para perfil web.', {
4087
+ action: 'sticker_pack_my_profile_owner_candidates_failed',
4088
+ google_sub: normalizedSub,
4089
+ error: error?.message,
4090
+ });
4091
+ }
4092
+ }
4093
+
4094
+ for (const ownerJid of Array.from(candidates)) {
4095
+ const identity = extractUserIdInfo(ownerJid);
4096
+ if (identity?.lid) lidCandidates.add(identity.lid);
4097
+ if (!identity?.lid && !identity?.jid) continue;
4098
+ const resolved = await resolveUserId(identity).catch(() => null);
4099
+ if (!resolved) continue;
4100
+ appendCandidate(resolved);
4101
+ const resolvedIdentity = extractUserIdInfo(resolved);
4102
+ if (resolvedIdentity?.lid) lidCandidates.add(resolvedIdentity.lid);
4103
+ }
4104
+
4105
+ const lookupValues = Array.from(lookupByJid).filter(Boolean);
4106
+ for (let offset = 0; offset < lookupValues.length; offset += 200) {
4107
+ const chunk = lookupValues.slice(offset, offset + 200);
4108
+ if (!chunk.length) continue;
4109
+ const placeholders = chunk.map(() => '?').join(', ');
4110
+ const rows = await executeQuery(
4111
+ `SELECT lid, jid
4112
+ FROM ${TABLES.LID_MAP}
4113
+ WHERE jid IN (${placeholders})
4114
+ ORDER BY last_seen DESC
4115
+ LIMIT 500`,
4116
+ chunk,
4117
+ ).catch(() => []);
4118
+
4119
+ for (const row of Array.isArray(rows) ? rows : []) {
4120
+ appendCandidate(row?.jid || '');
4121
+ const resolvedLid = normalizeJid(row?.lid || '');
4122
+ if (resolvedLid) lidCandidates.add(resolvedLid);
4123
+ }
4124
+ }
4125
+
4126
+ for (const lid of lidCandidates) {
4127
+ const resolved = await resolveUserId(extractUserIdInfo(lid)).catch(() => null);
4128
+ if (resolved) {
4129
+ appendCandidate(resolved);
4130
+ appendCandidate(lid);
4131
+ }
4132
+ }
4133
+
4134
+ const filtered = [];
4135
+ for (const candidate of Array.from(candidates)) {
4136
+ const normalized = normalizeJid(candidate) || '';
4137
+ if (!normalized || !normalized.includes('@')) continue;
4138
+ if (blockedJids.has(normalized)) continue;
4139
+
4140
+ const directPhone = toWhatsAppPhoneDigits(normalized);
4141
+ if (directPhone && blockedPhones.has(directPhone)) continue;
4142
+
4143
+ const isGoogleOwner = normalized.endsWith('@google.oauth');
4144
+ if (trustedPhones.size === 0) {
4145
+ filtered.push(normalized);
4146
+ continue;
4147
+ }
4148
+
4149
+ if (directPhone) {
4150
+ if (!trustedPhones.has(directPhone)) continue;
4151
+ filtered.push(normalized);
4152
+ continue;
4153
+ }
4154
+
4155
+ const resolved = await resolveUserId(extractUserIdInfo(normalized)).catch(() => null);
4156
+ const resolvedPhone = toWhatsAppPhoneDigits(resolved || '');
4157
+ if (resolvedPhone) {
4158
+ if (!trustedPhones.has(resolvedPhone) || blockedPhones.has(resolvedPhone)) continue;
4159
+ filtered.push(normalized);
4160
+ continue;
4161
+ }
4162
+
4163
+ if (isGoogleOwner) {
4164
+ filtered.push(normalized);
4165
+ }
4166
+ }
4167
+
4168
+ return Array.from(new Set(filtered));
4169
+ };
4170
+
4171
+ const handleMyProfileRequest = async (req, res, url = null) => {
4172
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
4173
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
4174
+ return;
4175
+ }
3644
4176
 
3645
4177
  const session = await resolveGoogleWebSessionFromRequest(req);
3646
4178
  const authGoogle = {
@@ -3669,7 +4201,65 @@ const handleMyProfileRequest = async (req, res) => {
3669
4201
  return;
3670
4202
  }
3671
4203
 
3672
- const packs = await listStickerPacksByOwner(session.ownerJid, { limit: 200, offset: 0 });
4204
+ const ownerCandidates = await resolveMyProfileOwnerCandidates(session);
4205
+ if (!ownerCandidates.length) {
4206
+ sendJson(req, res, 200, {
4207
+ data: {
4208
+ auth: { google: authGoogle },
4209
+ session: mapGoogleSessionResponseData(session),
4210
+ owner_jid: session.ownerJid,
4211
+ owner_jids: [],
4212
+ packs: [],
4213
+ stats: {
4214
+ total: 0,
4215
+ published: 0,
4216
+ drafts: 0,
4217
+ private: 0,
4218
+ unlisted: 0,
4219
+ public: 0,
4220
+ },
4221
+ },
4222
+ });
4223
+ return;
4224
+ }
4225
+
4226
+ const ownerPacks = await Promise.all(
4227
+ ownerCandidates.map((ownerJid) => listStickerPacksByOwner(ownerJid, { limit: 200, offset: 0 })),
4228
+ );
4229
+ const includeAutoPacks = parseEnvBool(
4230
+ url?.searchParams?.get('include_auto'),
4231
+ parseEnvBool(process.env.STICKER_WEB_MY_PROFILE_INCLUDE_AUTO_PACKS, false),
4232
+ );
4233
+
4234
+ const dedupPacks = new Map();
4235
+ for (const packList of ownerPacks) {
4236
+ for (const pack of Array.isArray(packList) ? packList : []) {
4237
+ if (!pack?.id) continue;
4238
+ if (!includeAutoPacks && pack?.is_auto_pack === true) continue;
4239
+ const existing = dedupPacks.get(pack.id);
4240
+ if (!existing) {
4241
+ dedupPacks.set(pack.id, pack);
4242
+ continue;
4243
+ }
4244
+ const currentUpdatedAt = Date.parse(String(pack.updated_at || pack.created_at || ''));
4245
+ const existingUpdatedAt = Date.parse(String(existing.updated_at || existing.created_at || ''));
4246
+ if (Number.isFinite(currentUpdatedAt) && (!Number.isFinite(existingUpdatedAt) || currentUpdatedAt > existingUpdatedAt)) {
4247
+ dedupPacks.set(pack.id, pack);
4248
+ }
4249
+ }
4250
+ }
4251
+
4252
+ const packs = Array.from(dedupPacks.values())
4253
+ .sort((a, b) => {
4254
+ const aUpdatedAt = Date.parse(String(a?.updated_at || a?.created_at || ''));
4255
+ const bUpdatedAt = Date.parse(String(b?.updated_at || b?.created_at || ''));
4256
+ if (!Number.isFinite(aUpdatedAt) && !Number.isFinite(bUpdatedAt)) return 0;
4257
+ if (!Number.isFinite(aUpdatedAt)) return 1;
4258
+ if (!Number.isFinite(bUpdatedAt)) return -1;
4259
+ return bUpdatedAt - aUpdatedAt;
4260
+ })
4261
+ .slice(0, 300);
4262
+
3673
4263
  const engagementByPackId = await listStickerPackEngagementByPackIds(packs.map((pack) => pack.id));
3674
4264
 
3675
4265
  const mappedPacks = packs.map((pack) => {
@@ -3702,6 +4292,7 @@ const handleMyProfileRequest = async (req, res) => {
3702
4292
  auth: { google: authGoogle },
3703
4293
  session: mapGoogleSessionResponseData(session),
3704
4294
  owner_jid: session.ownerJid,
4295
+ owner_jids: ownerCandidates,
3705
4296
  packs: mappedPacks,
3706
4297
  stats,
3707
4298
  },
@@ -3715,6 +4306,9 @@ const invalidateStickerCatalogDerivedCaches = () => {
3715
4306
  GLOBAL_RANK_CACHE.value = null;
3716
4307
  GLOBAL_RANK_CACHE.pending = null;
3717
4308
  HOME_MARKETPLACE_STATS_CACHE.clear();
4309
+ CATALOG_LIST_CACHE.clear();
4310
+ CATALOG_CREATOR_RANKING_CACHE.clear();
4311
+ CATALOG_PACK_PAYLOAD_CACHE.clear();
3718
4312
  SYSTEM_SUMMARY_CACHE.expiresAt = 0;
3719
4313
  SYSTEM_SUMMARY_CACHE.value = null;
3720
4314
  SYSTEM_SUMMARY_CACHE.pending = null;
@@ -3891,23 +4485,39 @@ const loadOwnedPackForWebManagement = async (req, res, packKey, { allowMissing =
3891
4485
  return null;
3892
4486
  }
3893
4487
 
3894
- try {
3895
- const pack = await stickerPackService.getPackInfo({
3896
- ownerJid: session.ownerJid,
3897
- identifier: normalizedPackKey,
3898
- });
3899
- return { session, packKey: normalizedPackKey, pack };
3900
- } catch (error) {
3901
- if (allowMissing && error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
3902
- return { session, packKey: normalizedPackKey, pack: null, missing: true };
4488
+ const ownerCandidatesRaw = await resolveMyProfileOwnerCandidates(session).catch(() => []);
4489
+ const ownerCandidates = Array.from(new Set([normalizeJid(session.ownerJid) || '', ...ownerCandidatesRaw].filter(Boolean)));
4490
+ const fallbackOwnerJid = ownerCandidates[0] || normalizeJid(session.ownerJid) || '';
4491
+
4492
+ for (const ownerJid of ownerCandidates) {
4493
+ try {
4494
+ const pack = await stickerPackService.getPackInfo({
4495
+ ownerJid,
4496
+ identifier: normalizedPackKey,
4497
+ });
4498
+ return { session, ownerJid, ownerCandidates, packKey: normalizedPackKey, pack };
4499
+ } catch (error) {
4500
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
4501
+ continue;
4502
+ }
4503
+ const mapped = mapStickerPackWebManageError(error);
4504
+ sendJson(req, res, mapped.statusCode, {
4505
+ error: mapped.message,
4506
+ code: mapped.code,
4507
+ });
4508
+ return null;
3903
4509
  }
3904
- const mapped = mapStickerPackWebManageError(error);
3905
- sendJson(req, res, mapped.statusCode, {
3906
- error: mapped.message,
3907
- code: mapped.code,
3908
- });
3909
- return null;
3910
4510
  }
4511
+
4512
+ if (allowMissing) {
4513
+ return { session, ownerJid: fallbackOwnerJid, ownerCandidates, packKey: normalizedPackKey, pack: null, missing: true };
4514
+ }
4515
+
4516
+ sendJson(req, res, 404, {
4517
+ error: 'Pack nao encontrado para este usuario.',
4518
+ code: STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND,
4519
+ });
4520
+ return null;
3911
4521
  };
3912
4522
 
3913
4523
  const buildManagedPackAnalytics = async (pack) => {
@@ -3990,7 +4600,7 @@ const handleManagedPackRequest = async (req, res, packKey) => {
3990
4600
  const isMutableMethod = req.method === 'PATCH' || req.method === 'DELETE';
3991
4601
  const context = await loadOwnedPackForWebManagement(req, res, packKey, { allowMissing: isMutableMethod });
3992
4602
  if (!context) return;
3993
- const { session, packKey: normalizedPackKey } = context;
4603
+ const { packKey: normalizedPackKey } = context;
3994
4604
 
3995
4605
  if (req.method === 'GET' || req.method === 'HEAD') {
3996
4606
  await sendManagedPackResponse(req, res, context.pack);
@@ -4008,7 +4618,7 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4008
4618
 
4009
4619
  try {
4010
4620
  const result = await deleteManagedPackWithCleanup({
4011
- ownerJid: session.ownerJid,
4621
+ ownerJid: context.ownerJid,
4012
4622
  identifier: normalizedPackKey,
4013
4623
  fallbackPack: context.pack,
4014
4624
  });
@@ -4059,13 +4669,13 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4059
4669
  const currentName = sanitizeText(updatedPack?.name, PACK_CREATE_MAX_NAME_LENGTH, { allowEmpty: false });
4060
4670
  if (!nextName) {
4061
4671
  updatedPack = await stickerPackService.renamePack({
4062
- ownerJid: session.ownerJid,
4672
+ ownerJid: context.ownerJid,
4063
4673
  identifier: normalizedPackKey,
4064
4674
  name: payload.name,
4065
4675
  });
4066
4676
  } else if (nextName !== currentName) {
4067
4677
  updatedPack = await stickerPackService.renamePack({
4068
- ownerJid: session.ownerJid,
4678
+ ownerJid: context.ownerJid,
4069
4679
  identifier: normalizedPackKey,
4070
4680
  name: payload.name,
4071
4681
  });
@@ -4078,13 +4688,13 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4078
4688
  const currentPublisher = sanitizeText(updatedPack?.publisher, PACK_CREATE_MAX_PUBLISHER_LENGTH, { allowEmpty: false });
4079
4689
  if (!nextPublisher) {
4080
4690
  updatedPack = await stickerPackService.setPackPublisher({
4081
- ownerJid: session.ownerJid,
4691
+ ownerJid: context.ownerJid,
4082
4692
  identifier: normalizedPackKey,
4083
4693
  publisher: payload.publisher,
4084
4694
  });
4085
4695
  } else if (nextPublisher !== currentPublisher) {
4086
4696
  updatedPack = await stickerPackService.setPackPublisher({
4087
- ownerJid: session.ownerJid,
4697
+ ownerJid: context.ownerJid,
4088
4698
  identifier: normalizedPackKey,
4089
4699
  publisher: payload.publisher,
4090
4700
  });
@@ -4097,13 +4707,13 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4097
4707
  const currentVisibility = String(updatedPack?.visibility || '').trim().toLowerCase();
4098
4708
  if (!nextVisibility) {
4099
4709
  updatedPack = await stickerPackService.setPackVisibility({
4100
- ownerJid: session.ownerJid,
4710
+ ownerJid: context.ownerJid,
4101
4711
  identifier: normalizedPackKey,
4102
4712
  visibility: payload.visibility,
4103
4713
  });
4104
4714
  } else if (nextVisibility !== currentVisibility) {
4105
4715
  updatedPack = await stickerPackService.setPackVisibility({
4106
- ownerJid: session.ownerJid,
4716
+ ownerJid: context.ownerJid,
4107
4717
  identifier: normalizedPackKey,
4108
4718
  visibility: payload.visibility,
4109
4719
  });
@@ -4119,7 +4729,7 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4119
4729
  const currentDescriptionWithTags = buildPackDescriptionWithTags(currentMeta.cleanDescription || '', currentMeta.tags);
4120
4730
  if (String(descriptionWithTags || '') !== String(currentDescriptionWithTags || '')) {
4121
4731
  updatedPack = await stickerPackService.setPackDescription({
4122
- ownerJid: session.ownerJid,
4732
+ ownerJid: context.ownerJid,
4123
4733
  identifier: normalizedPackKey,
4124
4734
  description: descriptionWithTags || '',
4125
4735
  });
@@ -4167,7 +4777,7 @@ const handleManagedPackCloneRequest = async (req, res, packKey) => {
4167
4777
 
4168
4778
  try {
4169
4779
  const cloned = await stickerPackService.clonePack({
4170
- ownerJid: context.session.ownerJid,
4780
+ ownerJid: context.ownerJid,
4171
4781
  identifier: context.packKey,
4172
4782
  newName,
4173
4783
  });
@@ -4205,7 +4815,7 @@ const handleManagedPackCoverRequest = async (req, res, packKey) => {
4205
4815
 
4206
4816
  try {
4207
4817
  const updated = await stickerPackService.setPackCover({
4208
- ownerJid: context.session.ownerJid,
4818
+ ownerJid: context.ownerJid,
4209
4819
  identifier: context.packKey,
4210
4820
  stickerId: payload?.sticker_id,
4211
4821
  });
@@ -4218,7 +4828,7 @@ const handleManagedPackCoverRequest = async (req, res, packKey) => {
4218
4828
  }
4219
4829
  if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND) {
4220
4830
  const fresh = await stickerPackService
4221
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
4831
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4222
4832
  .catch(() => context.pack);
4223
4833
  await sendManagedPackMutationStatus(req, res, 'already_deleted', fresh, {
4224
4834
  pack_key: context.packKey,
@@ -4270,7 +4880,7 @@ const handleManagedPackReorderRequest = async (req, res, packKey) => {
4270
4880
 
4271
4881
  try {
4272
4882
  const updated = await stickerPackService.reorderPackItems({
4273
- ownerJid: context.session.ownerJid,
4883
+ ownerJid: context.ownerJid,
4274
4884
  identifier: context.packKey,
4275
4885
  orderStickerIds: requestedOrderIds,
4276
4886
  });
@@ -4283,7 +4893,7 @@ const handleManagedPackReorderRequest = async (req, res, packKey) => {
4283
4893
  }
4284
4894
  if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.INVALID_INPUT) {
4285
4895
  const fresh = await stickerPackService
4286
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
4896
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4287
4897
  .catch(() => context.pack);
4288
4898
  await sendManagedPackMutationStatus(req, res, 'noop', fresh, {
4289
4899
  pack_key: context.packKey,
@@ -4313,7 +4923,7 @@ const handleManagedPackStickerDeleteRequest = async (req, res, packKey, stickerI
4313
4923
 
4314
4924
  try {
4315
4925
  const result = await stickerPackService.removeStickerFromPack({
4316
- ownerJid: context.session.ownerJid,
4926
+ ownerJid: context.ownerJid,
4317
4927
  identifier: context.packKey,
4318
4928
  selector: stickerId,
4319
4929
  });
@@ -4336,7 +4946,7 @@ const handleManagedPackStickerDeleteRequest = async (req, res, packKey, stickerI
4336
4946
  }
4337
4947
  if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND) {
4338
4948
  const fresh = await stickerPackService
4339
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
4949
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4340
4950
  .catch(() => context.pack);
4341
4951
  await sendManagedPackMutationStatus(req, res, 'already_deleted', fresh, {
4342
4952
  pack_key: context.packKey,
@@ -4390,19 +5000,19 @@ const handleManagedPackStickerCreateRequest = async (req, res, packKey) => {
4390
5000
  let uploadedAssetId = '';
4391
5001
  try {
4392
5002
  const normalizedUpload = await convertUploadMediaToWebp({
4393
- ownerJid: context.session.ownerJid,
5003
+ ownerJid: context.ownerJid,
4394
5004
  buffer: decoded.buffer,
4395
5005
  mimetype: decoded.mimetype || 'image/webp',
4396
5006
  });
4397
5007
  const asset = await saveStickerAssetFromBuffer({
4398
- ownerJid: context.session.ownerJid,
5008
+ ownerJid: context.ownerJid,
4399
5009
  buffer: normalizedUpload.buffer,
4400
5010
  mimetype: normalizedUpload.mimetype || 'image/webp',
4401
5011
  });
4402
5012
  uploadedAssetId = String(asset?.id || '').trim();
4403
5013
 
4404
5014
  let updatedPack = await stickerPackService.addStickerToPack({
4405
- ownerJid: context.session.ownerJid,
5015
+ ownerJid: context.ownerJid,
4406
5016
  identifier: context.packKey,
4407
5017
  asset: { id: uploadedAssetId },
4408
5018
  emojis: [],
@@ -4411,7 +5021,7 @@ const handleManagedPackStickerCreateRequest = async (req, res, packKey) => {
4411
5021
 
4412
5022
  if (payload?.set_cover === true) {
4413
5023
  updatedPack = await stickerPackService.setPackCover({
4414
- ownerJid: context.session.ownerJid,
5024
+ ownerJid: context.ownerJid,
4415
5025
  identifier: context.packKey,
4416
5026
  stickerId: uploadedAssetId,
4417
5027
  });
@@ -4490,7 +5100,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4490
5100
  let uploadedAssetId = '';
4491
5101
  try {
4492
5102
  const originalPack = await stickerPackService.getPackInfo({
4493
- ownerJid: context.session.ownerJid,
5103
+ ownerJid: context.ownerJid,
4494
5104
  identifier: context.packKey,
4495
5105
  });
4496
5106
  const originalItems = Array.isArray(originalPack?.items) ? originalPack.items : [];
@@ -4504,12 +5114,12 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4504
5114
  }
4505
5115
 
4506
5116
  const normalizedUpload = await convertUploadMediaToWebp({
4507
- ownerJid: context.session.ownerJid,
5117
+ ownerJid: context.ownerJid,
4508
5118
  buffer: decoded.buffer,
4509
5119
  mimetype: decoded.mimetype || 'image/webp',
4510
5120
  });
4511
5121
  const asset = await saveStickerAssetFromBuffer({
4512
- ownerJid: context.session.ownerJid,
5122
+ ownerJid: context.ownerJid,
4513
5123
  buffer: normalizedUpload.buffer,
4514
5124
  mimetype: normalizedUpload.mimetype || 'image/webp',
4515
5125
  });
@@ -4525,7 +5135,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4525
5135
  }
4526
5136
 
4527
5137
  const swapResult = await runSqlTransaction(async (connection) => {
4528
- const packRow = await findStickerPackByOwnerAndIdentifier(context.session.ownerJid, context.packKey, { connection });
5138
+ const packRow = await findStickerPackByOwnerAndIdentifier(context.ownerJid, context.packKey, { connection });
4529
5139
  if (!packRow) return { status: 'pack_missing' };
4530
5140
 
4531
5141
  const liveOldItem = await getStickerPackItemByStickerId(packRow.id, normalizedStickerId, connection);
@@ -4577,7 +5187,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4577
5187
 
4578
5188
  if (swapResult?.status === 'old_sticker_missing') {
4579
5189
  const fresh = await stickerPackService
4580
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
5190
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4581
5191
  .catch(() => originalPack);
4582
5192
  await cleanupOrphanStickerAssets(uploadedAssetId ? [uploadedAssetId] : [], { reason: 'replace_sticker_old_missing' });
4583
5193
  await sendManagedPackMutationStatus(req, res, 'already_deleted', fresh, {
@@ -4589,7 +5199,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4589
5199
 
4590
5200
  if (swapResult?.status === 'duplicate_target') {
4591
5201
  const fresh = await stickerPackService
4592
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
5202
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4593
5203
  .catch(() => originalPack);
4594
5204
  await sendManagedPackMutationStatus(req, res, 'noop', fresh, {
4595
5205
  pack_key: context.packKey,
@@ -4602,7 +5212,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4602
5212
 
4603
5213
  invalidateStickerCatalogDerivedCaches();
4604
5214
  const finalPack = await stickerPackService.getPackInfo({
4605
- ownerJid: context.session.ownerJid,
5215
+ ownerJid: context.ownerJid,
4606
5216
  identifier: context.packKey,
4607
5217
  });
4608
5218
  await cleanupOrphanStickerAssets([normalizedStickerId], { reason: 'replace_sticker_old_cleanup' });
@@ -4715,55 +5325,37 @@ const handleCreatePackRequest = async (req, res) => {
4715
5325
  const manualTags = mergeUniqueTags(Array.isArray(payload?.tags) ? payload.tags : []).slice(0, 8);
4716
5326
  const persistedDescription = buildPackDescriptionWithTags(description, manualTags);
4717
5327
  const visibility = String(payload?.visibility || 'public').trim().toLowerCase();
4718
- const explicitOwnerJid = toOwnerJid(payload?.owner_jid);
4719
5328
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
4720
- let googleCreator = null;
5329
+ if (!googleSession?.ownerJid || !googleSession?.sub) {
5330
+ sendJson(req, res, 401, {
5331
+ error: 'Sessão expirada ou ausente. Faça login novamente para criar packs.',
5332
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
5333
+ });
5334
+ return;
5335
+ }
4721
5336
 
4722
- if (googleSession) {
4723
- googleCreator = {
4724
- ownerJid: googleSession.ownerJid,
5337
+ try {
5338
+ await assertGoogleIdentityNotBanned({
4725
5339
  sub: googleSession.sub,
4726
5340
  email: googleSession.email,
4727
- name: googleSession.name,
4728
- picture: googleSession.picture,
4729
- };
4730
- } else if (STICKER_WEB_GOOGLE_AUTH_REQUIRED || payload?.google_id_token) {
4731
- try {
4732
- const googleClaims = await verifyGoogleIdToken(payload?.google_id_token);
4733
- const googleOwnerJid = buildGoogleOwnerJid(googleClaims.sub);
4734
- await assertGoogleIdentityNotBanned({
4735
- sub: googleClaims.sub,
4736
- email: googleClaims.email,
4737
- ownerJid: googleOwnerJid,
4738
- });
4739
- if (!googleOwnerJid) {
4740
- sendJson(req, res, 400, {
4741
- error: 'Não foi possível vincular a conta Google ao criador.',
4742
- code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
4743
- });
4744
- return;
4745
- }
4746
- googleCreator = {
4747
- ownerJid: googleOwnerJid,
4748
- ...googleClaims,
4749
- };
4750
- } catch (error) {
4751
- sendJson(req, res, Number(error?.statusCode || 401), {
4752
- error: error?.message || 'Login Google inválido.',
4753
- code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
4754
- });
4755
- return;
4756
- }
4757
- }
4758
-
4759
- if (STICKER_WEB_GOOGLE_AUTH_REQUIRED && !googleCreator) {
4760
- sendJson(req, res, 400, {
4761
- error: 'Faça login com Google para criar packs nesta página.',
4762
- code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
5341
+ ownerJid: googleSession.ownerJid,
5342
+ });
5343
+ } catch (error) {
5344
+ sendJson(req, res, Number(error?.statusCode || 403), {
5345
+ error: error?.message || 'Conta sem permissão para criar packs.',
5346
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
4763
5347
  });
4764
5348
  return;
4765
5349
  }
4766
5350
 
5351
+ const googleCreator = {
5352
+ ownerJid: googleSession.ownerJid,
5353
+ sub: googleSession.sub,
5354
+ email: googleSession.email,
5355
+ name: googleSession.name,
5356
+ picture: googleSession.picture,
5357
+ };
5358
+
4767
5359
  if (googleCreator?.sub && googleCreator?.ownerJid) {
4768
5360
  await upsertGoogleWebUserRecord({
4769
5361
  sub: googleCreator.sub,
@@ -4779,17 +5371,7 @@ const handleCreatePackRequest = async (req, res) => {
4779
5371
  });
4780
5372
  }
4781
5373
 
4782
- const ownerJid = googleCreator?.ownerJid || explicitOwnerJid;
4783
-
4784
- if (!ownerJid) {
4785
- sendJson(req, res, 400, {
4786
- error: STICKER_WEB_GOOGLE_AUTH_REQUIRED
4787
- ? 'Faça login com Google para criar packs nesta página.'
4788
- : 'Não foi possível resolver owner_jid para criar o pack.',
4789
- code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
4790
- });
4791
- return;
4792
- }
5374
+ const ownerJid = googleCreator.ownerJid;
4793
5375
 
4794
5376
  try {
4795
5377
  logPackWebFlow('info', 'create_pack_start', {
@@ -5390,46 +5972,63 @@ const handleCreatorRankingRequest = async (req, res, url) => {
5390
5972
  const limit = clampInt(url.searchParams.get('limit'), 50, 5, 200);
5391
5973
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
5392
5974
  const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
5393
-
5394
- const { packs } = await listStickerPacksForCatalog({
5975
+ const cacheKey = buildCacheKey([
5976
+ 'creator_ranking',
5395
5977
  visibility,
5396
- search: q,
5397
- limit: 120,
5398
- offset: 0,
5399
- });
5400
- const driftSnapshot = await getMarketplaceDriftSnapshot();
5401
- const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
5402
- const ranking = buildCreatorRanking(
5403
- STICKER_CATALOG_ONLY_CLASSIFIED ? entries.filter((entry) => isPackClassified(entry.packClassification)) : entries,
5404
- { limit },
5405
- );
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
+ );
5406
6001
 
5407
- sendJson(req, res, 200, {
5408
- data: ranking.map((creator) => ({
5409
- creator_score: Number(
5410
- (
5411
- Number(creator.avg_pack_score || 0) * 0.45 +
5412
- Number(creator.total_likes || 0) * 0.0008 +
5413
- Number(creator.total_opens || 0) * 0.00015
5414
- ).toFixed(6),
5415
- ),
5416
- publisher: creator.publisher,
5417
- verified: Boolean(creator.verified),
5418
- badges: creator.verified ? ['verified_creator'] : [],
5419
- stats: {
5420
- packs_count: Number(creator.packs_count || 0),
5421
- total_likes: Number(creator.total_likes || 0),
5422
- total_opens: Number(creator.total_opens || 0),
5423
- avg_pack_score: Number(creator.avg_pack_score || 0),
5424
- },
5425
- top_pack: creator.top_pack ? toSummaryEntry(creator.top_pack, { hideSensitiveCover: !hasNsfwAccess }) : null,
5426
- })),
5427
- filters: {
5428
- visibility,
5429
- q,
5430
- 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
+ };
5431
6028
  },
5432
6029
  });
6030
+
6031
+ sendJson(req, res, 200, payload);
5433
6032
  };
5434
6033
 
5435
6034
  const handleRecommendationsRequest = async (req, res, url) => {
@@ -5581,6 +6180,7 @@ const buildSystemSummarySnapshot = async () => {
5581
6180
 
5582
6181
  const socketReadyState = Number(activeSocket?.ws?.readyState);
5583
6182
  const botJid = resolveBotJid(activeSocket?.user?.id) || null;
6183
+ const botPhone = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '') || null;
5584
6184
  const botConnected = Boolean(botJid) && socketReadyState === 1;
5585
6185
  const botConnectionStatus = botConnected
5586
6186
  ? 'online'
@@ -5646,6 +6246,7 @@ const buildSystemSummarySnapshot = async () => {
5646
6246
  connected: botConnected,
5647
6247
  connection_status: botConnectionStatus,
5648
6248
  jid: botJid,
6249
+ phone: botPhone,
5649
6250
  ready_state: Number.isFinite(socketReadyState) ? socketReadyState : null,
5650
6251
  },
5651
6252
  platform,
@@ -6510,6 +7111,15 @@ const handleSupportInfoRequest = async (req, res) => {
6510
7111
  sendJson(req, res, 200, { data });
6511
7112
  };
6512
7113
 
7114
+ const handleBotContactInfoRequest = async (req, res) => {
7115
+ const data = buildBotContactInfo();
7116
+ if (!data) {
7117
+ sendJson(req, res, 404, { error: 'Contato do bot indisponivel no momento.' });
7118
+ return;
7119
+ }
7120
+ sendJson(req, res, 200, { data });
7121
+ };
7122
+
6513
7123
  const handlePublicDataAssetRequest = async (req, res, pathname) => {
6514
7124
  const suffix = pathname.slice(STICKER_DATA_PUBLIC_PATH.length).replace(/^\/+/, '');
6515
7125
  if (!suffix) {
@@ -6560,42 +7170,60 @@ const handlePublicDataAssetRequest = async (req, res, pathname) => {
6560
7170
  };
6561
7171
 
6562
7172
  const fetchPublicPackPayload = async (normalizedPackKey) => {
6563
- const pack = await findStickerPackByPackKey(normalizedPackKey);
6564
- if (!pack || !isPackPubliclyVisible(pack)) return null;
6565
-
6566
- const items = await listStickerPackItems(pack.id);
6567
- const stickerIds = items.map((item) => item.sticker_id);
6568
- const [classifications, packClassification, engagement] = await Promise.all([
6569
- listStickerClassificationsByAssetIds(stickerIds),
6570
- getPackClassificationSummaryByAssetIds(stickerIds),
6571
- getStickerPackEngagementByPackId(pack.id),
6572
- ]);
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
+ }
6573
7195
 
6574
- if (STICKER_CATALOG_ONLY_CLASSIFIED && !isPackClassified(packClassification)) {
6575
- return null;
6576
- }
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
+ });
6577
7216
 
6578
- const interactionStatsByPack = await listStickerPackInteractionStatsByPackIds([pack.id]);
6579
- const driftSnapshot = await getMarketplaceDriftSnapshot();
6580
- const byAssetClassification = new Map(classifications.map((entry) => [entry.asset_id, entry]));
6581
- const orderedClassifications = stickerIds.map((stickerId) => byAssetClassification.get(stickerId)).filter(Boolean);
6582
- const signals = computePackSignals({
6583
- pack: { ...pack, items },
6584
- engagement,
6585
- packClassification,
6586
- itemClassifications: orderedClassifications,
6587
- interactionStats: interactionStatsByPack.get(pack.id) || null,
6588
- scoringWeights: driftSnapshot.weights,
7217
+ return {
7218
+ pack,
7219
+ items,
7220
+ byAssetClassification,
7221
+ packClassification,
7222
+ engagement,
7223
+ signals,
7224
+ };
7225
+ },
6589
7226
  });
6590
-
6591
- return {
6592
- pack,
6593
- items,
6594
- byAssetClassification,
6595
- packClassification,
6596
- engagement,
6597
- signals,
6598
- };
6599
7227
  };
6600
7228
 
6601
7229
  const handleDetailsRequest = async (req, res, packKey, url) => {
@@ -6686,6 +7314,23 @@ const handleAssetRequest = async (req, res, packKey, stickerToken) => {
6686
7314
  return;
6687
7315
  }
6688
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
+
6689
7334
  if (decorated) {
6690
7335
  res.setHeader('X-Sticker-Category', String(decorated?.category || 'unknown'));
6691
7336
  res.setHeader('X-Sticker-NSFW', decorated?.is_nsfw ? '1' : '0');
@@ -6759,7 +7404,7 @@ const handlePackInteractionRequest = async (req, res, packKey, interaction, url)
6759
7404
  const listAdminActiveGoogleWebSessions = async ({ limit = 200 } = {}) => {
6760
7405
  const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
6761
7406
  const rows = await executeQuery(
6762
- `SELECT session_token, google_sub, owner_jid, email, name, picture_url, created_at, last_seen_at, expires_at
7407
+ `SELECT session_token, google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, last_seen_at, expires_at
6763
7408
  FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
6764
7409
  WHERE revoked_at IS NULL
6765
7410
  AND expires_at > UTC_TIMESTAMP()
@@ -6770,6 +7415,7 @@ const listAdminActiveGoogleWebSessions = async ({ limit = 200 } = {}) => {
6770
7415
  session_token: String(row.session_token || '').trim(),
6771
7416
  google_sub: normalizeGoogleSubject(row.google_sub),
6772
7417
  owner_jid: normalizeJid(row.owner_jid) || null,
7418
+ owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
6773
7419
  email: normalizeEmail(row.email) || null,
6774
7420
  name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
6775
7421
  picture: String(row.picture_url || '').trim() || null,
@@ -6782,7 +7428,7 @@ const listAdminActiveGoogleWebSessions = async ({ limit = 200 } = {}) => {
6782
7428
  const listAdminKnownGoogleUsers = async ({ limit = 200 } = {}) => {
6783
7429
  const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
6784
7430
  const rows = await executeQuery(
6785
- `SELECT google_sub, owner_jid, email, name, picture_url, created_at, updated_at, last_login_at, last_seen_at
7431
+ `SELECT google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, updated_at, last_login_at, last_seen_at
6786
7432
  FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
6787
7433
  ORDER BY COALESCE(last_seen_at, last_login_at, updated_at, created_at) DESC
6788
7434
  LIMIT ${safeLimit}`,
@@ -6790,6 +7436,7 @@ const listAdminKnownGoogleUsers = async ({ limit = 200 } = {}) => {
6790
7436
  return (Array.isArray(rows) ? rows : []).map((row) => ({
6791
7437
  google_sub: normalizeGoogleSubject(row.google_sub),
6792
7438
  owner_jid: normalizeJid(row.owner_jid) || null,
7439
+ owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
6793
7440
  email: normalizeEmail(row.email) || null,
6794
7441
  name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
6795
7442
  picture: String(row.picture_url || '').trim() || null,
@@ -7291,314 +7938,60 @@ const handleAdminBanRevokeRequest = async (req, res, banId) => {
7291
7938
  }
7292
7939
  };
7293
7940
 
7294
- const handleCatalogApiRequest = async (req, res, pathname, url) => {
7295
- if (pathname === `${STICKER_API_BASE_PATH}/create`) {
7296
- if (req.method !== 'POST') {
7297
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7298
- return true;
7299
- }
7300
- await handleCreatePackRequest(req, res);
7301
- return true;
7302
- }
7303
-
7304
- if (pathname === `${STICKER_API_BASE_PATH}/auth/google/session`) {
7305
- await handleGoogleAuthSessionRequest(req, res);
7306
- return true;
7307
- }
7308
-
7309
- if (pathname === `${STICKER_API_BASE_PATH}/me`) {
7310
- await handleMyProfileRequest(req, res);
7311
- return true;
7312
- }
7313
-
7314
- if (pathname === `${STICKER_API_BASE_PATH}/admin/session`) {
7315
- await handleAdminPanelSessionRequest(req, res);
7316
- return true;
7317
- }
7318
-
7319
- if (pathname === STICKER_API_BASE_PATH) {
7320
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7321
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7322
- return true;
7323
- }
7324
- await handleListRequest(req, res, url);
7325
- return true;
7326
- }
7327
-
7328
- if (pathname === `${STICKER_API_BASE_PATH}/intents`) {
7329
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7330
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7331
- return true;
7332
- }
7333
- await handleIntentCollectionsRequest(req, res, url);
7334
- return true;
7335
- }
7336
-
7337
- if (pathname === `${STICKER_API_BASE_PATH}/creators`) {
7338
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7339
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7340
- return true;
7341
- }
7342
- await handleCreatorRankingRequest(req, res, url);
7343
- return true;
7344
- }
7345
-
7346
- if (pathname === `${STICKER_API_BASE_PATH}/recommendations`) {
7347
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7348
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7349
- return true;
7350
- }
7351
- await handleRecommendationsRequest(req, res, url);
7352
- return true;
7353
- }
7354
-
7355
- if (pathname === `${STICKER_API_BASE_PATH}/stats`) {
7356
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7357
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7358
- return true;
7359
- }
7360
- await handleMarketplaceStatsRequest(req, res, url);
7361
- return true;
7362
- }
7363
-
7364
- if (pathname === `${STICKER_API_BASE_PATH}/create-config`) {
7365
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7366
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7367
- return true;
7368
- }
7369
- await handleCreatePackConfigRequest(req, res);
7370
- return true;
7371
- }
7372
-
7373
- if (pathname === STICKER_ORPHAN_API_PATH) {
7374
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7375
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7376
- return true;
7377
- }
7378
- await handleOrphanStickerListRequest(req, res, url);
7379
- return true;
7380
- }
7381
-
7382
- const suffix = pathname.slice(STICKER_API_BASE_PATH.length).replace(/^\/+/, '');
7383
- if (!suffix) return false;
7384
-
7385
- const segments = suffix.split('/').filter(Boolean).map((segment) => {
7386
- try {
7387
- return decodeURIComponent(segment);
7388
- } catch {
7389
- return segment;
7390
- }
7391
- });
7392
-
7393
- if (segments.length === 1 && segments[0] === 'data-files') {
7394
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7395
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7396
- return true;
7397
- }
7398
- await handleDataFileListRequest(req, res, url);
7399
- return true;
7400
- }
7401
-
7402
- if (segments.length === 1 && segments[0] === 'system-summary') {
7403
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7404
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7405
- return true;
7406
- }
7407
- await handleSystemSummaryRequest(req, res);
7408
- return true;
7409
- }
7410
-
7411
- if (segments.length === 1 && segments[0] === 'project-summary') {
7412
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7413
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7414
- return true;
7415
- }
7416
- await handleGitHubProjectSummaryRequest(req, res);
7417
- return true;
7418
- }
7419
-
7420
- if (segments.length === 1 && segments[0] === 'global-ranking-summary') {
7421
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7422
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7423
- return true;
7424
- }
7425
- await handleGlobalRankingSummaryRequest(req, res);
7426
- return true;
7427
- }
7428
-
7429
- if (segments.length === 1 && segments[0] === 'readme-summary') {
7430
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7431
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7432
- return true;
7433
- }
7434
- await handleReadmeSummaryRequest(req, res);
7435
- return true;
7436
- }
7437
-
7438
- if (segments.length === 1 && segments[0] === 'readme-markdown') {
7439
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7440
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7441
- return true;
7442
- }
7443
- await handleReadmeMarkdownRequest(req, res);
7444
- return true;
7445
- }
7446
-
7447
- if (segments.length === 1 && segments[0] === 'support') {
7448
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7449
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7450
- return true;
7451
- }
7452
- await handleSupportInfoRequest(req, res);
7453
- return true;
7454
- }
7455
-
7456
- if (segments[0] === 'admin') {
7457
- if (segments.length === 2 && segments[1] === 'overview') {
7458
- await handleAdminOverviewRequest(req, res);
7459
- return true;
7460
- }
7461
- if (segments.length === 2 && segments[1] === 'users') {
7462
- await handleAdminUsersRequest(req, res, url);
7463
- return true;
7464
- }
7465
- if (segments.length === 2 && segments[1] === 'moderators') {
7466
- await handleAdminModeratorsRequest(req, res);
7467
- return true;
7468
- }
7469
- if (segments.length === 3 && segments[1] === 'moderators') {
7470
- await handleAdminModeratorDeleteRequest(req, res, segments[2]);
7471
- return true;
7472
- }
7473
- if (segments.length === 2 && segments[1] === 'packs') {
7474
- await handleAdminPacksRequest(req, res, url);
7475
- return true;
7476
- }
7477
- if (segments.length === 3 && segments[1] === 'packs') {
7478
- await handleAdminPackDetailsRequest(req, res, segments[2]);
7479
- return true;
7480
- }
7481
- if (segments.length === 4 && segments[1] === 'packs' && segments[3] === 'delete') {
7482
- await handleAdminPackDeleteRequest(req, res, segments[2]);
7483
- return true;
7484
- }
7485
- if (segments.length === 6 && segments[1] === 'packs' && segments[3] === 'stickers' && segments[5] === 'delete') {
7486
- await handleAdminPackStickerDeleteRequest(req, res, segments[2], segments[4]);
7487
- return true;
7488
- }
7489
- if (segments.length === 4 && segments[1] === 'stickers' && segments[3] === 'delete') {
7490
- await handleAdminGlobalStickerDeleteRequest(req, res, segments[2]);
7491
- return true;
7492
- }
7493
- if (segments.length === 2 && segments[1] === 'bans') {
7494
- await handleAdminBansRequest(req, res);
7495
- return true;
7496
- }
7497
- if (segments.length === 4 && segments[1] === 'bans' && segments[3] === 'revoke') {
7498
- await handleAdminBanRevokeRequest(req, res, segments[2]);
7499
- return true;
7500
- }
7501
- sendJson(req, res, 404, { error: 'Rota admin nao encontrada.' });
7502
- return true;
7503
- }
7504
-
7505
- if (segments.length === 1) {
7506
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7507
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7508
- return true;
7509
- }
7510
- await handleDetailsRequest(req, res, segments[0], url);
7511
- return true;
7512
- }
7513
-
7514
- if (segments.length === 2 && ['open', 'like', 'dislike'].includes(segments[1])) {
7515
- if (req.method !== 'POST') {
7516
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7517
- return true;
7518
- }
7519
- await handlePackInteractionRequest(req, res, segments[0], segments[1], url);
7520
- return true;
7521
- }
7522
-
7523
- if (segments.length === 2 && segments[1] === 'manage') {
7524
- await handleManagedPackRequest(req, res, segments[0]);
7525
- return true;
7526
- }
7527
-
7528
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'clone') {
7529
- await handleManagedPackCloneRequest(req, res, segments[0]);
7530
- return true;
7531
- }
7532
-
7533
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'cover') {
7534
- await handleManagedPackCoverRequest(req, res, segments[0]);
7535
- return true;
7536
- }
7537
-
7538
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'reorder') {
7539
- await handleManagedPackReorderRequest(req, res, segments[0]);
7540
- return true;
7541
- }
7542
-
7543
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'analytics') {
7544
- await handleManagedPackAnalyticsRequest(req, res, segments[0]);
7545
- return true;
7546
- }
7547
-
7548
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'stickers') {
7549
- await handleManagedPackStickerCreateRequest(req, res, segments[0]);
7550
- return true;
7551
- }
7552
-
7553
- if (segments.length === 4 && segments[1] === 'manage' && segments[2] === 'stickers') {
7554
- await handleManagedPackStickerDeleteRequest(req, res, segments[0], segments[3]);
7555
- return true;
7556
- }
7557
-
7558
- if (segments.length === 5 && segments[1] === 'manage' && segments[2] === 'stickers' && segments[4] === 'replace') {
7559
- await handleManagedPackStickerReplaceRequest(req, res, segments[0], segments[3]);
7560
- return true;
7561
- }
7562
-
7563
- if (segments.length === 2 && segments[1] === 'publish-state') {
7564
- if (!['GET', 'HEAD', 'POST'].includes(req.method || '')) {
7565
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7566
- return true;
7567
- }
7568
- await handlePackPublishStateRequest(req, res, segments[0], url);
7569
- return true;
7570
- }
7571
-
7572
- if (segments.length === 2 && segments[1] === 'finalize') {
7573
- if (req.method !== 'POST') {
7574
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7575
- return true;
7576
- }
7577
- await handleFinalizePackRequest(req, res, segments[0]);
7578
- return true;
7579
- }
7580
-
7581
- if (segments.length === 2 && segments[1] === 'stickers-upload') {
7582
- if (req.method !== 'POST') {
7583
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7584
- return true;
7585
- }
7586
- await handleUploadStickerToPackRequest(req, res, segments[0]);
7587
- return true;
7588
- }
7589
-
7590
- if (segments.length === 3 && segments[1] === 'stickers') {
7591
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7592
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7593
- return true;
7594
- }
7595
- await handleAssetRequest(req, res, segments[0], segments[2]);
7596
- return true;
7597
- }
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
+ });
7598
7992
 
7599
- sendJson(req, res, 404, { error: 'Rota de sticker pack nao encontrada.' });
7600
- return true;
7601
- };
7993
+ const handleCatalogApiRequest = async (req, res, pathname, url) =>
7994
+ catalogApiRouter({ req, res, pathname, url });
7602
7995
 
7603
7996
  const handleCatalogPageRequest = async (req, res, pathname) => {
7604
7997
  const normalizedPath = pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
@@ -7624,6 +8017,20 @@ const handleCatalogPageRequest = async (req, res, pathname) => {
7624
8017
 
7625
8018
  if (normalizedPath === STICKER_CREATE_WEB_PATH) {
7626
8019
  try {
8020
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
8021
+ if (!googleSession?.ownerJid) {
8022
+ const requestUrl = new URL(req.url || `${STICKER_CREATE_WEB_PATH}/`, SITE_ORIGIN);
8023
+ const nextPath = `${requestUrl.pathname}${requestUrl.search}`;
8024
+ const loginRedirectUrl = new URL(`${STICKER_LOGIN_WEB_PATH}/`, SITE_ORIGIN);
8025
+ loginRedirectUrl.searchParams.set('next', nextPath);
8026
+
8027
+ res.statusCode = 302;
8028
+ res.setHeader('Location', `${loginRedirectUrl.pathname}${loginRedirectUrl.search}`);
8029
+ res.setHeader('Cache-Control', 'no-store');
8030
+ res.end();
8031
+ return;
8032
+ }
8033
+
7627
8034
  const html = await renderCreatePackHtml();
7628
8035
  sendText(req, res, 200, html, 'text/html; charset=utf-8');
7629
8036
  return;
@@ -7711,6 +8118,7 @@ export const isStickerCatalogEnabled = () => STICKER_CATALOG_ENABLED;
7711
8118
  export const getStickerCatalogConfig = () => ({
7712
8119
  enabled: STICKER_CATALOG_ENABLED,
7713
8120
  webPath: STICKER_WEB_PATH,
8121
+ userProfilePath: USER_PROFILE_WEB_PATH,
7714
8122
  apiBasePath: STICKER_API_BASE_PATH,
7715
8123
  orphanApiPath: STICKER_ORPHAN_API_PATH,
7716
8124
  dataPublicPath: STICKER_DATA_PUBLIC_PATH,
@@ -7734,6 +8142,29 @@ export async function maybeHandleStickerCatalogRequest(req, res, { pathname, url
7734
8142
  if (!['GET', 'HEAD', 'POST', 'PATCH', 'DELETE'].includes(req.method || '')) return false;
7735
8143
  if (maybeRedirectToCanonicalHost(req, res, url)) return true;
7736
8144
 
8145
+ if (pathname === USER_PROFILE_WEB_PATH || pathname === `${USER_PROFILE_WEB_PATH}/`) {
8146
+ if (!['GET', 'HEAD'].includes(req.method || '')) return false;
8147
+ try {
8148
+ const html = await renderUserDashboardHtml();
8149
+ res.setHeader('Cache-Control', 'no-store');
8150
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
8151
+ sendText(req, res, 200, html, 'text/html; charset=utf-8');
8152
+ } catch (error) {
8153
+ if (error?.code === 'ENOENT') {
8154
+ sendJson(req, res, 404, { error: 'Template da pagina de usuario nao encontrado.' });
8155
+ return true;
8156
+ }
8157
+
8158
+ logger.error('Falha ao renderizar pagina de usuario.', {
8159
+ action: 'user_dashboard_page_render_failed',
8160
+ path: pathname,
8161
+ error: error?.message,
8162
+ });
8163
+ sendJson(req, res, 500, { error: 'Falha interna ao renderizar pagina de usuario.' });
8164
+ }
8165
+ return true;
8166
+ }
8167
+
7737
8168
  if (pathname === '/sitemap.xml') {
7738
8169
  if (!['GET', 'HEAD'].includes(req.method || '')) return false;
7739
8170
  try {