@kaikybrofc/omnizap-system 2.2.3 → 2.2.4

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.
@@ -8,6 +8,7 @@ import { getJidUser, normalizeJid, resolveBotJid } from '../../config/baileysCon
8
8
  import { getAdminPhone, getAdminRawValue, resolveAdminJid } from '../../config/adminIdentity.js';
9
9
  import { getActiveSocket } from '../../services/socketState.js';
10
10
  import { extractUserIdInfo, resolveUserId } from '../../services/lidMapService.js';
11
+ import { resolveWhatsAppOwnerJidFromLoginPayload, toWhatsAppOwnerJid, toWhatsAppPhoneDigits } from '../../services/whatsappLoginLinkService.js';
11
12
  import logger from '../../utils/logger/loggerModule.js';
12
13
  import { getSystemMetrics } from '../../utils/systemMetrics/systemMetricsModule.js';
13
14
  import {
@@ -143,12 +144,15 @@ const STICKER_API_BASE_PATH = normalizeBasePath(process.env.STICKER_API_BASE_PAT
143
144
  const STICKER_ORPHAN_API_PATH = `${STICKER_API_BASE_PATH}/orphan-stickers`;
144
145
  const STICKER_CREATE_WEB_PATH = `${STICKER_WEB_PATH}/create`;
145
146
  const STICKER_ADMIN_WEB_PATH = `${STICKER_WEB_PATH}/admin`;
147
+ const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_PATH, '/login');
148
+ const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PATH, '/user');
146
149
  const STICKER_DATA_PUBLIC_PATH = normalizeBasePath(process.env.STICKER_DATA_PUBLIC_PATH, '/data');
147
150
  const STICKER_DATA_PUBLIC_DIR = path.resolve(process.env.STICKER_DATA_PUBLIC_DIR || path.join(process.cwd(), 'data'));
148
151
  const CATALOG_PUBLIC_DIR = path.resolve(process.cwd(), 'public');
149
152
  const CATALOG_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'index.html');
150
153
  const CREATE_PACK_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'create', 'index.html');
151
154
  const ADMIN_PANEL_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'admin', 'index.html');
155
+ const USER_DASHBOARD_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'user', 'index.html');
152
156
  const CATALOG_STYLES_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'css', 'styles.css');
153
157
  const CATALOG_SCRIPT_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'js', 'catalog.js');
154
158
  const DEFAULT_LIST_LIMIT = clampInt(process.env.STICKER_WEB_LIST_LIMIT, 24, 1, 60);
@@ -920,6 +924,7 @@ const normalizeGoogleWebSessionRow = (row) => {
920
924
  const token = String(row.session_token || '').trim();
921
925
  const sub = normalizeGoogleSubject(row.google_sub);
922
926
  const ownerJid = normalizeJid(row.owner_jid) || '';
927
+ const ownerPhone = toWhatsAppPhoneDigits(row.owner_phone || ownerJid) || '';
923
928
  const expiresAt = Number(new Date(row.expires_at || 0));
924
929
  if (!token || !sub || !ownerJid || !Number.isFinite(expiresAt)) return null;
925
930
  const createdAtRaw = Number(new Date(row.created_at || 0));
@@ -931,6 +936,7 @@ const normalizeGoogleWebSessionRow = (row) => {
931
936
  name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
932
937
  picture: String(row.picture_url || '').trim() || null,
933
938
  ownerJid,
939
+ ownerPhone,
934
940
  createdAt: Number.isFinite(createdAtRaw) ? createdAtRaw : Date.now(),
935
941
  expiresAt,
936
942
  lastSeenAt: Number.isFinite(lastSeenAtRaw) ? lastSeenAtRaw : 0,
@@ -959,10 +965,20 @@ const upsertGoogleWebUserRecord = async (user, connection = null) => {
959
965
  const sub = normalizeGoogleSubject(user?.sub);
960
966
  const ownerJid = normalizeJid(user?.ownerJid) || '';
961
967
  if (!sub || !ownerJid) return;
968
+ const ownerPhone = toWhatsAppPhoneDigits(ownerJid) || null;
962
969
  const email = String(user?.email || '').trim().toLowerCase() || null;
963
970
  const name = sanitizeText(user?.name || '', 120, { allowEmpty: true }) || null;
964
971
  const pictureUrl = String(user?.picture || '').trim().slice(0, 1024) || null;
965
972
 
973
+ // owner_jid e unico; removemos vinculos antigos desse numero antes do upsert para manter 1:1.
974
+ await executeQuery(
975
+ `DELETE FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
976
+ WHERE owner_jid = ?
977
+ AND google_sub <> ?`,
978
+ [ownerJid, sub],
979
+ connection,
980
+ );
981
+
966
982
  await executeQuery(
967
983
  `INSERT INTO ${TABLES.STICKER_WEB_GOOGLE_USER}
968
984
  (google_sub, owner_jid, email, name, picture_url, last_login_at, last_seen_at)
@@ -977,12 +993,22 @@ const upsertGoogleWebUserRecord = async (user, connection = null) => {
977
993
  [sub, ownerJid, email, name, pictureUrl],
978
994
  connection,
979
995
  );
996
+
997
+ // Persistimos o telefone normalizado do owner para facilitar consultas administrativas.
998
+ await executeQuery(
999
+ `UPDATE ${TABLES.STICKER_WEB_GOOGLE_USER}
1000
+ SET owner_phone = COALESCE(?, owner_phone)
1001
+ WHERE google_sub = ?`,
1002
+ [ownerPhone, sub],
1003
+ connection,
1004
+ ).catch(() => {});
980
1005
  };
981
1006
 
982
1007
  const upsertGoogleWebSessionRecord = async (session, connection = null) => {
983
1008
  const token = String(session?.token || '').trim();
984
1009
  const sub = normalizeGoogleSubject(session?.sub);
985
1010
  const ownerJid = normalizeJid(session?.ownerJid) || '';
1011
+ const ownerPhone = toWhatsAppPhoneDigits(session?.ownerPhone || ownerJid) || null;
986
1012
  const expiresAt = Number(session?.expiresAt || 0);
987
1013
  if (!token || !sub || !ownerJid || !Number.isFinite(expiresAt) || expiresAt <= 0) return;
988
1014
  const email = String(session?.email || '').trim().toLowerCase() || null;
@@ -1005,6 +1031,42 @@ const upsertGoogleWebSessionRecord = async (session, connection = null) => {
1005
1031
  [token, sub, ownerJid, email, name, pictureUrl, new Date(expiresAt)],
1006
1032
  connection,
1007
1033
  );
1034
+
1035
+ await executeQuery(
1036
+ `UPDATE ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1037
+ SET owner_phone = COALESCE(?, owner_phone)
1038
+ WHERE session_token = ?`,
1039
+ [ownerPhone, token],
1040
+ connection,
1041
+ ).catch(() => {});
1042
+ };
1043
+
1044
+ const deleteOtherGoogleWebSessionsInDb = async ({ token = '', ownerJid = '', sub = '' } = {}, connection = null) => {
1045
+ const sessionToken = String(token || '').trim();
1046
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
1047
+ const normalizedSub = normalizeGoogleSubject(sub);
1048
+ if (!sessionToken || (!normalizedOwnerJid && !normalizedSub)) return 0;
1049
+
1050
+ const clauses = [];
1051
+ const params = [];
1052
+ if (normalizedOwnerJid) {
1053
+ clauses.push('owner_jid = ?');
1054
+ params.push(normalizedOwnerJid);
1055
+ }
1056
+ if (normalizedSub) {
1057
+ clauses.push('google_sub = ?');
1058
+ params.push(normalizedSub);
1059
+ }
1060
+ if (!clauses.length) return 0;
1061
+
1062
+ const result = await executeQuery(
1063
+ `DELETE FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1064
+ WHERE session_token <> ?
1065
+ AND (${clauses.join(' OR ')})`,
1066
+ [sessionToken, ...params],
1067
+ connection,
1068
+ );
1069
+ return Number(result?.affectedRows || 0);
1008
1070
  };
1009
1071
 
1010
1072
  const persistGoogleWebSessionToDb = async (session) => {
@@ -1021,6 +1083,14 @@ const persistGoogleWebSessionToDb = async (session) => {
1021
1083
  },
1022
1084
  connection,
1023
1085
  );
1086
+ await deleteOtherGoogleWebSessionsInDb(
1087
+ {
1088
+ token: session.token,
1089
+ ownerJid: session.ownerJid,
1090
+ sub: session.sub,
1091
+ },
1092
+ connection,
1093
+ );
1024
1094
  await upsertGoogleWebSessionRecord(session, connection);
1025
1095
  });
1026
1096
  };
@@ -1030,7 +1100,7 @@ const findGoogleWebSessionInDbByToken = async (sessionToken) => {
1030
1100
  if (!token) return null;
1031
1101
  await maybePruneExpiredGoogleSessionsFromDb();
1032
1102
  const rows = await executeQuery(
1033
- `SELECT session_token, google_sub, owner_jid, email, name, picture_url, created_at, expires_at, last_seen_at
1103
+ `SELECT session_token, google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, expires_at, last_seen_at
1034
1104
  FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1035
1105
  WHERE session_token = ?
1036
1106
  AND revoked_at IS NULL
@@ -1624,26 +1694,42 @@ const requireOwnerAdminPanelSession = (req, res) => {
1624
1694
  return session;
1625
1695
  };
1626
1696
 
1627
- const createGoogleWebSession = (claims) => {
1697
+ const createGoogleWebSession = (claims, { ownerJid } = {}) => {
1628
1698
  pruneExpiredGoogleSessions();
1629
1699
  const token = randomUUID();
1630
1700
  const now = Date.now();
1701
+ const resolvedOwnerJid = normalizeJid(ownerJid) || buildGoogleOwnerJid(claims.sub);
1702
+ const resolvedOwnerPhone = toWhatsAppPhoneDigits(resolvedOwnerJid) || '';
1631
1703
  const session = {
1632
1704
  token,
1633
1705
  sub: claims.sub,
1634
1706
  email: claims.email || null,
1635
1707
  name: claims.name || null,
1636
1708
  picture: claims.picture || null,
1637
- ownerJid: buildGoogleOwnerJid(claims.sub),
1709
+ ownerJid: resolvedOwnerJid,
1710
+ ownerPhone: resolvedOwnerPhone,
1638
1711
  createdAt: now,
1639
1712
  expiresAt: now + STICKER_WEB_GOOGLE_SESSION_TTL_MS,
1640
1713
  lastSeenAt: now,
1641
1714
  lastDbTouchAt: 0,
1642
1715
  };
1643
- webGoogleSessionMap.set(token, session);
1644
1716
  return session;
1645
1717
  };
1646
1718
 
1719
+ const activateGoogleWebSession = (session) => {
1720
+ if (!session?.token) return;
1721
+ pruneExpiredGoogleSessions();
1722
+ webGoogleSessionMap.set(session.token, session);
1723
+ for (const [token, existing] of webGoogleSessionMap.entries()) {
1724
+ if (!existing || token === session.token) continue;
1725
+ const sameOwner = normalizeJid(existing.ownerJid) === normalizeJid(session.ownerJid);
1726
+ const sameSub = normalizeGoogleSubject(existing.sub) === normalizeGoogleSubject(session.sub);
1727
+ if (sameOwner || sameSub) {
1728
+ webGoogleSessionMap.delete(token);
1729
+ }
1730
+ }
1731
+ };
1732
+
1647
1733
  const resolveGoogleWebSessionFromRequest = async (req) => {
1648
1734
  pruneExpiredGoogleSessions();
1649
1735
  const sessionToken = getGoogleWebSessionTokenFromRequest(req);
@@ -2266,6 +2352,27 @@ const buildSupportInfo = async () => {
2266
2352
  };
2267
2353
  };
2268
2354
 
2355
+ const buildBotContactInfo = () => {
2356
+ const phone = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '');
2357
+ if (!phone) return null;
2358
+ const loginText = String(process.env.WHATSAPP_LOGIN_TRIGGER || 'iniciar').trim() || 'iniciar';
2359
+ const menuText = `${PACK_COMMAND_PREFIX}menu`;
2360
+ const buildUrl = (text) =>
2361
+ `https://api.whatsapp.com/send/?phone=${encodeURIComponent(phone)}&text=${encodeURIComponent(
2362
+ String(text || '').trim(),
2363
+ )}&type=custom_url&app_absent=0`;
2364
+
2365
+ return {
2366
+ phone,
2367
+ login_text: loginText,
2368
+ menu_text: menuText,
2369
+ urls: {
2370
+ login: buildUrl(loginText),
2371
+ menu: buildUrl(menuText),
2372
+ },
2373
+ };
2374
+ };
2375
+
2269
2376
  const listDataImageFiles = async () => {
2270
2377
  const files = [];
2271
2378
  const queue = [STICKER_DATA_PUBLIC_DIR];
@@ -2869,6 +2976,7 @@ const renderCatalogHtml = async ({ initialPackKey }) => {
2869
2976
  __STICKER_WEB_PATH__: escapeHtmlAttribute(STICKER_WEB_PATH),
2870
2977
  __STICKER_API_BASE_PATH__: escapeHtmlAttribute(STICKER_API_BASE_PATH),
2871
2978
  __STICKER_ORPHAN_API_PATH__: escapeHtmlAttribute(buildOrphanStickersApiUrl()),
2979
+ __STICKER_LOGIN_WEB_PATH__: escapeHtmlAttribute(STICKER_LOGIN_WEB_PATH),
2872
2980
  __STICKER_DATA_PUBLIC_PATH__: escapeHtmlAttribute(STICKER_DATA_PUBLIC_PATH),
2873
2981
  __DEFAULT_LIST_LIMIT__: String(DEFAULT_LIST_LIMIT),
2874
2982
  __DEFAULT_ORPHAN_LIST_LIMIT__: String(DEFAULT_ORPHAN_LIST_LIMIT),
@@ -3014,11 +3122,12 @@ const renderPackSeoHtml = ({ packSummary }) => {
3014
3122
  data-web-path="${escapeHtmlAttribute(STICKER_WEB_PATH)}"
3015
3123
  data-api-base-path="${escapeHtmlAttribute(STICKER_API_BASE_PATH)}"
3016
3124
  data-orphan-api-path="${escapeHtmlAttribute(STICKER_ORPHAN_API_PATH)}"
3125
+ data-login-path="${escapeHtmlAttribute(STICKER_LOGIN_WEB_PATH)}"
3017
3126
  data-default-limit="${DEFAULT_LIST_LIMIT}"
3018
3127
  data-default-orphan-limit="${DEFAULT_ORPHAN_LIST_LIMIT}"
3019
3128
  data-initial-pack-key="${escapeHtmlAttribute(packSummary?.pack_key || '')}"
3020
3129
  ></div>
3021
- <script type="module" src="/js/apps/stickersApp.js?v=20260227-ui-hotfix-v5"></script>
3130
+ <script type="module" src="/js/apps/stickersApp.js?v=20260228-login-redirect-my-packs1"></script>
3022
3131
  </body>
3023
3132
  </html>`;
3024
3133
  };
@@ -3054,6 +3163,7 @@ const renderCreatePackHtml = async () => {
3054
3163
  const replacements = {
3055
3164
  __STICKER_WEB_PATH__: escapeHtmlAttribute(STICKER_WEB_PATH),
3056
3165
  __STICKER_CREATE_WEB_PATH__: escapeHtmlAttribute(STICKER_CREATE_WEB_PATH),
3166
+ __STICKER_LOGIN_WEB_PATH__: escapeHtmlAttribute(STICKER_LOGIN_WEB_PATH),
3057
3167
  __STICKER_API_BASE_PATH__: escapeHtmlAttribute(STICKER_API_BASE_PATH),
3058
3168
  __PACK_COMMAND_PREFIX__: escapeHtmlAttribute(PACK_COMMAND_PREFIX),
3059
3169
  __CURRENT_YEAR__: String(new Date().getFullYear()),
@@ -3082,6 +3192,23 @@ const renderAdminPanelHtml = async () => {
3082
3192
  return html;
3083
3193
  };
3084
3194
 
3195
+ const renderUserDashboardHtml = async () => {
3196
+ const template = await fs.readFile(USER_DASHBOARD_TEMPLATE_PATH, 'utf8');
3197
+ const replacements = {
3198
+ __STICKER_WEB_PATH__: escapeHtmlAttribute(STICKER_WEB_PATH),
3199
+ __STICKER_LOGIN_WEB_PATH__: escapeHtmlAttribute(STICKER_LOGIN_WEB_PATH),
3200
+ __STICKER_API_BASE_PATH__: escapeHtmlAttribute(STICKER_API_BASE_PATH),
3201
+ __USER_PROFILE_WEB_PATH__: escapeHtmlAttribute(USER_PROFILE_WEB_PATH),
3202
+ __CURRENT_YEAR__: String(new Date().getFullYear()),
3203
+ };
3204
+
3205
+ let html = template;
3206
+ for (const [token, value] of Object.entries(replacements)) {
3207
+ html = html.replaceAll(token, value);
3208
+ }
3209
+ return html;
3210
+ };
3211
+
3085
3212
  const buildSitemapXml = async () => {
3086
3213
  if (SITEMAP_CACHE.expiresAt > Date.now() && SITEMAP_CACHE.xml) {
3087
3214
  return SITEMAP_CACHE.xml;
@@ -3514,24 +3641,7 @@ const handleGoogleAuthSessionRequest = async (req, res) => {
3514
3641
  if (req.method === 'GET' || req.method === 'HEAD') {
3515
3642
  const session = await resolveGoogleWebSessionFromRequest(req);
3516
3643
  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
- },
3644
+ data: mapGoogleSessionResponseData(session),
3535
3645
  });
3536
3646
  return;
3537
3647
  }
@@ -3567,20 +3677,57 @@ const handleGoogleAuthSessionRequest = async (req, res) => {
3567
3677
 
3568
3678
  try {
3569
3679
  const claims = await verifyGoogleIdToken(payload?.google_id_token || payload?.id_token);
3680
+ const linkedOwner = resolveWhatsAppOwnerJidFromLoginPayload(payload);
3681
+ if (!linkedOwner.ownerJid) {
3682
+ if (!linkedOwner.hasPayload) {
3683
+ sendJson(req, res, 400, {
3684
+ error: 'Abra esta pagina pelo link enviado no WhatsApp. Envie "iniciar" no bot para gerar o link de login.',
3685
+ code: 'WHATSAPP_LOGIN_LINK_REQUIRED',
3686
+ reason: 'missing_link',
3687
+ });
3688
+ return;
3689
+ }
3690
+
3691
+ const reason = String(linkedOwner.reason || '').trim().toLowerCase();
3692
+ const isUnauthorizedAttempt = ['invalid_signature', 'missing_signature'].includes(reason);
3693
+ const statusCode = isUnauthorizedAttempt ? 403 : 400;
3694
+ const errorMessage =
3695
+ reason === 'expired'
3696
+ ? 'Link de login expirado. Envie "iniciar" novamente no WhatsApp.'
3697
+ : isUnauthorizedAttempt
3698
+ ? 'Tentativa de login sem permissao detectada. Gere um novo link enviando "iniciar" no privado do bot.'
3699
+ : 'Link de login invalido. Envie "iniciar" novamente no WhatsApp.';
3700
+
3701
+ logger.warn('Tentativa de login web bloqueada por validacao do link WhatsApp.', {
3702
+ action: 'sticker_pack_google_web_login_link_blocked',
3703
+ reason: reason || 'unknown',
3704
+ remote_ip: req.socket?.remoteAddress || null,
3705
+ user_agent: req.headers?.['user-agent'] || null,
3706
+ });
3707
+
3708
+ sendJson(req, res, statusCode, {
3709
+ error: errorMessage,
3710
+ code: 'WHATSAPP_LOGIN_LINK_INVALID',
3711
+ reason: reason || 'invalid_link',
3712
+ });
3713
+ return;
3714
+ }
3715
+ const ownerJid = linkedOwner.ownerJid;
3716
+
3570
3717
  await assertGoogleIdentityNotBanned({
3571
3718
  sub: claims.sub,
3572
3719
  email: claims.email,
3573
- ownerJid: buildGoogleOwnerJid(claims.sub),
3720
+ ownerJid,
3574
3721
  });
3575
- const session = createGoogleWebSession(claims);
3722
+ const session = createGoogleWebSession(claims, { ownerJid });
3576
3723
  if (!session.ownerJid) {
3577
3724
  sendJson(req, res, 400, { error: 'Nao foi possivel vincular a conta Google.' });
3578
3725
  return;
3579
3726
  }
3580
3727
  try {
3581
3728
  await persistGoogleWebSessionToDb(session);
3729
+ activateGoogleWebSession(session);
3582
3730
  } catch (persistError) {
3583
- webGoogleSessionMap.delete(session.token);
3584
3731
  logger.error('Falha ao persistir sessão Google web no banco.', {
3585
3732
  action: 'sticker_pack_google_web_session_db_persist_failed',
3586
3733
  error: persistError?.message,
@@ -3596,17 +3743,7 @@ const handleGoogleAuthSessionRequest = async (req, res) => {
3596
3743
  }),
3597
3744
  );
3598
3745
  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
- },
3746
+ data: mapGoogleSessionResponseData(session),
3610
3747
  });
3611
3748
  } catch (error) {
3612
3749
  sendJson(req, res, Number(error?.statusCode || 401), {
@@ -3627,16 +3764,195 @@ const mapGoogleSessionResponseData = (session) =>
3627
3764
  name: session.name,
3628
3765
  picture: session.picture,
3629
3766
  },
3767
+ owner_jid: session.ownerJid,
3768
+ owner_phone: toWhatsAppPhoneDigits(session.ownerPhone || session.ownerJid) || null,
3630
3769
  expires_at: toIsoOrNull(session.expiresAt),
3631
3770
  }
3632
3771
  : {
3633
3772
  authenticated: false,
3634
3773
  provider: 'google',
3635
3774
  user: null,
3775
+ owner_jid: null,
3776
+ owner_phone: null,
3636
3777
  expires_at: null,
3637
3778
  };
3638
3779
 
3639
- const handleMyProfileRequest = async (req, res) => {
3780
+ const buildOwnerLookupJids = (value) => {
3781
+ const normalized = normalizeJid(value) || '';
3782
+ if (!normalized || !normalized.includes('@')) return [];
3783
+ const lookup = new Set([normalized]);
3784
+ const phoneDigits = toWhatsAppPhoneDigits(normalized);
3785
+ if (!phoneDigits) return Array.from(lookup);
3786
+ lookup.add(normalizeJid(`${phoneDigits}@s.whatsapp.net`) || '');
3787
+ lookup.add(normalizeJid(`${phoneDigits}@c.us`) || '');
3788
+ lookup.add(normalizeJid(`${phoneDigits}@hosted`) || '');
3789
+ return Array.from(lookup).filter(Boolean);
3790
+ };
3791
+
3792
+ const appendMyProfileOwnerCandidate = (candidateSet, lookupSet, value) => {
3793
+ const normalized = normalizeJid(value) || '';
3794
+ if (!normalized || !normalized.includes('@')) return;
3795
+
3796
+ candidateSet.add(normalized);
3797
+ for (const lookupJid of buildOwnerLookupJids(normalized)) {
3798
+ lookupSet.add(lookupJid);
3799
+ }
3800
+
3801
+ const phoneOwner = toWhatsAppOwnerJid(value);
3802
+ if (phoneOwner) {
3803
+ candidateSet.add(phoneOwner);
3804
+ for (const lookupJid of buildOwnerLookupJids(phoneOwner)) {
3805
+ lookupSet.add(lookupJid);
3806
+ }
3807
+ }
3808
+ };
3809
+
3810
+ const buildPhoneSet = (...values) => {
3811
+ const set = new Set();
3812
+ for (const value of values) {
3813
+ const digits = toWhatsAppPhoneDigits(value);
3814
+ if (digits) set.add(digits);
3815
+ }
3816
+ return set;
3817
+ };
3818
+
3819
+ const resolveMyProfileOwnerCandidates = async (session) => {
3820
+ const candidates = new Set();
3821
+ const lookupByJid = new Set();
3822
+ const lidCandidates = new Set();
3823
+ const appendCandidate = (value) => appendMyProfileOwnerCandidate(candidates, lookupByJid, value);
3824
+ const trustedPhones = new Set();
3825
+ const blockedJids = new Set();
3826
+ const blockedPhones = new Set();
3827
+
3828
+ appendCandidate(session?.ownerJid);
3829
+ appendCandidate(toWhatsAppOwnerJid(session?.ownerPhone || session?.ownerJid));
3830
+ for (const phone of buildPhoneSet(session?.ownerPhone, session?.ownerJid)) {
3831
+ trustedPhones.add(phone);
3832
+ }
3833
+
3834
+ const activeSocket = getActiveSocket();
3835
+ const botJid = normalizeJid(resolveBotJid(activeSocket?.user?.id || '') || '');
3836
+ if (botJid) {
3837
+ blockedJids.add(botJid);
3838
+ for (const phone of buildPhoneSet(botJid)) {
3839
+ blockedPhones.add(phone);
3840
+ }
3841
+ }
3842
+
3843
+ const legacyGoogleOwner = buildGoogleOwnerJid(session?.sub);
3844
+ if (legacyGoogleOwner) appendCandidate(legacyGoogleOwner);
3845
+
3846
+ const sessionResolved = await resolveUserId(extractUserIdInfo(session?.ownerJid || session?.ownerPhone || null)).catch(() => null);
3847
+ if (sessionResolved) {
3848
+ appendCandidate(sessionResolved);
3849
+ for (const phone of buildPhoneSet(sessionResolved)) {
3850
+ trustedPhones.add(phone);
3851
+ }
3852
+ }
3853
+
3854
+ const normalizedSub = normalizeGoogleSubject(session?.sub);
3855
+ if (normalizedSub) {
3856
+ try {
3857
+ const rows = await executeQuery(
3858
+ `SELECT owner_jid, owner_phone
3859
+ FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
3860
+ WHERE google_sub = ?
3861
+ LIMIT 1`,
3862
+ [normalizedSub],
3863
+ );
3864
+ const row = Array.isArray(rows) ? rows[0] : null;
3865
+ appendCandidate(row?.owner_jid || '');
3866
+ appendCandidate(row?.owner_phone || '');
3867
+ const mappedResolved = await resolveUserId(extractUserIdInfo(row?.owner_jid || row?.owner_phone || null)).catch(() => null);
3868
+ if (mappedResolved) appendCandidate(mappedResolved);
3869
+ } catch (error) {
3870
+ logger.warn('Falha ao resolver owners para perfil web.', {
3871
+ action: 'sticker_pack_my_profile_owner_candidates_failed',
3872
+ google_sub: normalizedSub,
3873
+ error: error?.message,
3874
+ });
3875
+ }
3876
+ }
3877
+
3878
+ for (const ownerJid of Array.from(candidates)) {
3879
+ const identity = extractUserIdInfo(ownerJid);
3880
+ if (identity?.lid) lidCandidates.add(identity.lid);
3881
+ if (!identity?.lid && !identity?.jid) continue;
3882
+ const resolved = await resolveUserId(identity).catch(() => null);
3883
+ if (!resolved) continue;
3884
+ appendCandidate(resolved);
3885
+ const resolvedIdentity = extractUserIdInfo(resolved);
3886
+ if (resolvedIdentity?.lid) lidCandidates.add(resolvedIdentity.lid);
3887
+ }
3888
+
3889
+ const lookupValues = Array.from(lookupByJid).filter(Boolean);
3890
+ for (let offset = 0; offset < lookupValues.length; offset += 200) {
3891
+ const chunk = lookupValues.slice(offset, offset + 200);
3892
+ if (!chunk.length) continue;
3893
+ const placeholders = chunk.map(() => '?').join(', ');
3894
+ const rows = await executeQuery(
3895
+ `SELECT lid, jid
3896
+ FROM ${TABLES.LID_MAP}
3897
+ WHERE jid IN (${placeholders})
3898
+ ORDER BY last_seen DESC
3899
+ LIMIT 500`,
3900
+ chunk,
3901
+ ).catch(() => []);
3902
+
3903
+ for (const row of Array.isArray(rows) ? rows : []) {
3904
+ appendCandidate(row?.jid || '');
3905
+ const resolvedLid = normalizeJid(row?.lid || '');
3906
+ if (resolvedLid) lidCandidates.add(resolvedLid);
3907
+ }
3908
+ }
3909
+
3910
+ for (const lid of lidCandidates) {
3911
+ const resolved = await resolveUserId(extractUserIdInfo(lid)).catch(() => null);
3912
+ if (resolved) {
3913
+ appendCandidate(resolved);
3914
+ appendCandidate(lid);
3915
+ }
3916
+ }
3917
+
3918
+ const filtered = [];
3919
+ for (const candidate of Array.from(candidates)) {
3920
+ const normalized = normalizeJid(candidate) || '';
3921
+ if (!normalized || !normalized.includes('@')) continue;
3922
+ if (blockedJids.has(normalized)) continue;
3923
+
3924
+ const directPhone = toWhatsAppPhoneDigits(normalized);
3925
+ if (directPhone && blockedPhones.has(directPhone)) continue;
3926
+
3927
+ const isGoogleOwner = normalized.endsWith('@google.oauth');
3928
+ if (trustedPhones.size === 0) {
3929
+ filtered.push(normalized);
3930
+ continue;
3931
+ }
3932
+
3933
+ if (directPhone) {
3934
+ if (!trustedPhones.has(directPhone)) continue;
3935
+ filtered.push(normalized);
3936
+ continue;
3937
+ }
3938
+
3939
+ const resolved = await resolveUserId(extractUserIdInfo(normalized)).catch(() => null);
3940
+ const resolvedPhone = toWhatsAppPhoneDigits(resolved || '');
3941
+ if (resolvedPhone) {
3942
+ if (!trustedPhones.has(resolvedPhone) || blockedPhones.has(resolvedPhone)) continue;
3943
+ filtered.push(normalized);
3944
+ continue;
3945
+ }
3946
+
3947
+ if (isGoogleOwner) {
3948
+ filtered.push(normalized);
3949
+ }
3950
+ }
3951
+
3952
+ return Array.from(new Set(filtered));
3953
+ };
3954
+
3955
+ const handleMyProfileRequest = async (req, res, url = null) => {
3640
3956
  if (!['GET', 'HEAD'].includes(req.method || '')) {
3641
3957
  sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
3642
3958
  return;
@@ -3669,7 +3985,65 @@ const handleMyProfileRequest = async (req, res) => {
3669
3985
  return;
3670
3986
  }
3671
3987
 
3672
- const packs = await listStickerPacksByOwner(session.ownerJid, { limit: 200, offset: 0 });
3988
+ const ownerCandidates = await resolveMyProfileOwnerCandidates(session);
3989
+ if (!ownerCandidates.length) {
3990
+ sendJson(req, res, 200, {
3991
+ data: {
3992
+ auth: { google: authGoogle },
3993
+ session: mapGoogleSessionResponseData(session),
3994
+ owner_jid: session.ownerJid,
3995
+ owner_jids: [],
3996
+ packs: [],
3997
+ stats: {
3998
+ total: 0,
3999
+ published: 0,
4000
+ drafts: 0,
4001
+ private: 0,
4002
+ unlisted: 0,
4003
+ public: 0,
4004
+ },
4005
+ },
4006
+ });
4007
+ return;
4008
+ }
4009
+
4010
+ const ownerPacks = await Promise.all(
4011
+ ownerCandidates.map((ownerJid) => listStickerPacksByOwner(ownerJid, { limit: 200, offset: 0 })),
4012
+ );
4013
+ const includeAutoPacks = parseEnvBool(
4014
+ url?.searchParams?.get('include_auto'),
4015
+ parseEnvBool(process.env.STICKER_WEB_MY_PROFILE_INCLUDE_AUTO_PACKS, false),
4016
+ );
4017
+
4018
+ const dedupPacks = new Map();
4019
+ for (const packList of ownerPacks) {
4020
+ for (const pack of Array.isArray(packList) ? packList : []) {
4021
+ if (!pack?.id) continue;
4022
+ if (!includeAutoPacks && pack?.is_auto_pack === true) continue;
4023
+ const existing = dedupPacks.get(pack.id);
4024
+ if (!existing) {
4025
+ dedupPacks.set(pack.id, pack);
4026
+ continue;
4027
+ }
4028
+ const currentUpdatedAt = Date.parse(String(pack.updated_at || pack.created_at || ''));
4029
+ const existingUpdatedAt = Date.parse(String(existing.updated_at || existing.created_at || ''));
4030
+ if (Number.isFinite(currentUpdatedAt) && (!Number.isFinite(existingUpdatedAt) || currentUpdatedAt > existingUpdatedAt)) {
4031
+ dedupPacks.set(pack.id, pack);
4032
+ }
4033
+ }
4034
+ }
4035
+
4036
+ const packs = Array.from(dedupPacks.values())
4037
+ .sort((a, b) => {
4038
+ const aUpdatedAt = Date.parse(String(a?.updated_at || a?.created_at || ''));
4039
+ const bUpdatedAt = Date.parse(String(b?.updated_at || b?.created_at || ''));
4040
+ if (!Number.isFinite(aUpdatedAt) && !Number.isFinite(bUpdatedAt)) return 0;
4041
+ if (!Number.isFinite(aUpdatedAt)) return 1;
4042
+ if (!Number.isFinite(bUpdatedAt)) return -1;
4043
+ return bUpdatedAt - aUpdatedAt;
4044
+ })
4045
+ .slice(0, 300);
4046
+
3673
4047
  const engagementByPackId = await listStickerPackEngagementByPackIds(packs.map((pack) => pack.id));
3674
4048
 
3675
4049
  const mappedPacks = packs.map((pack) => {
@@ -3702,6 +4076,7 @@ const handleMyProfileRequest = async (req, res) => {
3702
4076
  auth: { google: authGoogle },
3703
4077
  session: mapGoogleSessionResponseData(session),
3704
4078
  owner_jid: session.ownerJid,
4079
+ owner_jids: ownerCandidates,
3705
4080
  packs: mappedPacks,
3706
4081
  stats,
3707
4082
  },
@@ -3891,23 +4266,39 @@ const loadOwnedPackForWebManagement = async (req, res, packKey, { allowMissing =
3891
4266
  return null;
3892
4267
  }
3893
4268
 
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 };
4269
+ const ownerCandidatesRaw = await resolveMyProfileOwnerCandidates(session).catch(() => []);
4270
+ const ownerCandidates = Array.from(new Set([normalizeJid(session.ownerJid) || '', ...ownerCandidatesRaw].filter(Boolean)));
4271
+ const fallbackOwnerJid = ownerCandidates[0] || normalizeJid(session.ownerJid) || '';
4272
+
4273
+ for (const ownerJid of ownerCandidates) {
4274
+ try {
4275
+ const pack = await stickerPackService.getPackInfo({
4276
+ ownerJid,
4277
+ identifier: normalizedPackKey,
4278
+ });
4279
+ return { session, ownerJid, ownerCandidates, packKey: normalizedPackKey, pack };
4280
+ } catch (error) {
4281
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
4282
+ continue;
4283
+ }
4284
+ const mapped = mapStickerPackWebManageError(error);
4285
+ sendJson(req, res, mapped.statusCode, {
4286
+ error: mapped.message,
4287
+ code: mapped.code,
4288
+ });
4289
+ return null;
3903
4290
  }
3904
- const mapped = mapStickerPackWebManageError(error);
3905
- sendJson(req, res, mapped.statusCode, {
3906
- error: mapped.message,
3907
- code: mapped.code,
3908
- });
3909
- return null;
3910
4291
  }
4292
+
4293
+ if (allowMissing) {
4294
+ return { session, ownerJid: fallbackOwnerJid, ownerCandidates, packKey: normalizedPackKey, pack: null, missing: true };
4295
+ }
4296
+
4297
+ sendJson(req, res, 404, {
4298
+ error: 'Pack nao encontrado para este usuario.',
4299
+ code: STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND,
4300
+ });
4301
+ return null;
3911
4302
  };
3912
4303
 
3913
4304
  const buildManagedPackAnalytics = async (pack) => {
@@ -3990,7 +4381,7 @@ const handleManagedPackRequest = async (req, res, packKey) => {
3990
4381
  const isMutableMethod = req.method === 'PATCH' || req.method === 'DELETE';
3991
4382
  const context = await loadOwnedPackForWebManagement(req, res, packKey, { allowMissing: isMutableMethod });
3992
4383
  if (!context) return;
3993
- const { session, packKey: normalizedPackKey } = context;
4384
+ const { packKey: normalizedPackKey } = context;
3994
4385
 
3995
4386
  if (req.method === 'GET' || req.method === 'HEAD') {
3996
4387
  await sendManagedPackResponse(req, res, context.pack);
@@ -4008,7 +4399,7 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4008
4399
 
4009
4400
  try {
4010
4401
  const result = await deleteManagedPackWithCleanup({
4011
- ownerJid: session.ownerJid,
4402
+ ownerJid: context.ownerJid,
4012
4403
  identifier: normalizedPackKey,
4013
4404
  fallbackPack: context.pack,
4014
4405
  });
@@ -4059,13 +4450,13 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4059
4450
  const currentName = sanitizeText(updatedPack?.name, PACK_CREATE_MAX_NAME_LENGTH, { allowEmpty: false });
4060
4451
  if (!nextName) {
4061
4452
  updatedPack = await stickerPackService.renamePack({
4062
- ownerJid: session.ownerJid,
4453
+ ownerJid: context.ownerJid,
4063
4454
  identifier: normalizedPackKey,
4064
4455
  name: payload.name,
4065
4456
  });
4066
4457
  } else if (nextName !== currentName) {
4067
4458
  updatedPack = await stickerPackService.renamePack({
4068
- ownerJid: session.ownerJid,
4459
+ ownerJid: context.ownerJid,
4069
4460
  identifier: normalizedPackKey,
4070
4461
  name: payload.name,
4071
4462
  });
@@ -4078,13 +4469,13 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4078
4469
  const currentPublisher = sanitizeText(updatedPack?.publisher, PACK_CREATE_MAX_PUBLISHER_LENGTH, { allowEmpty: false });
4079
4470
  if (!nextPublisher) {
4080
4471
  updatedPack = await stickerPackService.setPackPublisher({
4081
- ownerJid: session.ownerJid,
4472
+ ownerJid: context.ownerJid,
4082
4473
  identifier: normalizedPackKey,
4083
4474
  publisher: payload.publisher,
4084
4475
  });
4085
4476
  } else if (nextPublisher !== currentPublisher) {
4086
4477
  updatedPack = await stickerPackService.setPackPublisher({
4087
- ownerJid: session.ownerJid,
4478
+ ownerJid: context.ownerJid,
4088
4479
  identifier: normalizedPackKey,
4089
4480
  publisher: payload.publisher,
4090
4481
  });
@@ -4097,13 +4488,13 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4097
4488
  const currentVisibility = String(updatedPack?.visibility || '').trim().toLowerCase();
4098
4489
  if (!nextVisibility) {
4099
4490
  updatedPack = await stickerPackService.setPackVisibility({
4100
- ownerJid: session.ownerJid,
4491
+ ownerJid: context.ownerJid,
4101
4492
  identifier: normalizedPackKey,
4102
4493
  visibility: payload.visibility,
4103
4494
  });
4104
4495
  } else if (nextVisibility !== currentVisibility) {
4105
4496
  updatedPack = await stickerPackService.setPackVisibility({
4106
- ownerJid: session.ownerJid,
4497
+ ownerJid: context.ownerJid,
4107
4498
  identifier: normalizedPackKey,
4108
4499
  visibility: payload.visibility,
4109
4500
  });
@@ -4119,7 +4510,7 @@ const handleManagedPackRequest = async (req, res, packKey) => {
4119
4510
  const currentDescriptionWithTags = buildPackDescriptionWithTags(currentMeta.cleanDescription || '', currentMeta.tags);
4120
4511
  if (String(descriptionWithTags || '') !== String(currentDescriptionWithTags || '')) {
4121
4512
  updatedPack = await stickerPackService.setPackDescription({
4122
- ownerJid: session.ownerJid,
4513
+ ownerJid: context.ownerJid,
4123
4514
  identifier: normalizedPackKey,
4124
4515
  description: descriptionWithTags || '',
4125
4516
  });
@@ -4167,7 +4558,7 @@ const handleManagedPackCloneRequest = async (req, res, packKey) => {
4167
4558
 
4168
4559
  try {
4169
4560
  const cloned = await stickerPackService.clonePack({
4170
- ownerJid: context.session.ownerJid,
4561
+ ownerJid: context.ownerJid,
4171
4562
  identifier: context.packKey,
4172
4563
  newName,
4173
4564
  });
@@ -4205,7 +4596,7 @@ const handleManagedPackCoverRequest = async (req, res, packKey) => {
4205
4596
 
4206
4597
  try {
4207
4598
  const updated = await stickerPackService.setPackCover({
4208
- ownerJid: context.session.ownerJid,
4599
+ ownerJid: context.ownerJid,
4209
4600
  identifier: context.packKey,
4210
4601
  stickerId: payload?.sticker_id,
4211
4602
  });
@@ -4218,7 +4609,7 @@ const handleManagedPackCoverRequest = async (req, res, packKey) => {
4218
4609
  }
4219
4610
  if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND) {
4220
4611
  const fresh = await stickerPackService
4221
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
4612
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4222
4613
  .catch(() => context.pack);
4223
4614
  await sendManagedPackMutationStatus(req, res, 'already_deleted', fresh, {
4224
4615
  pack_key: context.packKey,
@@ -4270,7 +4661,7 @@ const handleManagedPackReorderRequest = async (req, res, packKey) => {
4270
4661
 
4271
4662
  try {
4272
4663
  const updated = await stickerPackService.reorderPackItems({
4273
- ownerJid: context.session.ownerJid,
4664
+ ownerJid: context.ownerJid,
4274
4665
  identifier: context.packKey,
4275
4666
  orderStickerIds: requestedOrderIds,
4276
4667
  });
@@ -4283,7 +4674,7 @@ const handleManagedPackReorderRequest = async (req, res, packKey) => {
4283
4674
  }
4284
4675
  if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.INVALID_INPUT) {
4285
4676
  const fresh = await stickerPackService
4286
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
4677
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4287
4678
  .catch(() => context.pack);
4288
4679
  await sendManagedPackMutationStatus(req, res, 'noop', fresh, {
4289
4680
  pack_key: context.packKey,
@@ -4313,7 +4704,7 @@ const handleManagedPackStickerDeleteRequest = async (req, res, packKey, stickerI
4313
4704
 
4314
4705
  try {
4315
4706
  const result = await stickerPackService.removeStickerFromPack({
4316
- ownerJid: context.session.ownerJid,
4707
+ ownerJid: context.ownerJid,
4317
4708
  identifier: context.packKey,
4318
4709
  selector: stickerId,
4319
4710
  });
@@ -4336,7 +4727,7 @@ const handleManagedPackStickerDeleteRequest = async (req, res, packKey, stickerI
4336
4727
  }
4337
4728
  if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND) {
4338
4729
  const fresh = await stickerPackService
4339
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
4730
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4340
4731
  .catch(() => context.pack);
4341
4732
  await sendManagedPackMutationStatus(req, res, 'already_deleted', fresh, {
4342
4733
  pack_key: context.packKey,
@@ -4390,19 +4781,19 @@ const handleManagedPackStickerCreateRequest = async (req, res, packKey) => {
4390
4781
  let uploadedAssetId = '';
4391
4782
  try {
4392
4783
  const normalizedUpload = await convertUploadMediaToWebp({
4393
- ownerJid: context.session.ownerJid,
4784
+ ownerJid: context.ownerJid,
4394
4785
  buffer: decoded.buffer,
4395
4786
  mimetype: decoded.mimetype || 'image/webp',
4396
4787
  });
4397
4788
  const asset = await saveStickerAssetFromBuffer({
4398
- ownerJid: context.session.ownerJid,
4789
+ ownerJid: context.ownerJid,
4399
4790
  buffer: normalizedUpload.buffer,
4400
4791
  mimetype: normalizedUpload.mimetype || 'image/webp',
4401
4792
  });
4402
4793
  uploadedAssetId = String(asset?.id || '').trim();
4403
4794
 
4404
4795
  let updatedPack = await stickerPackService.addStickerToPack({
4405
- ownerJid: context.session.ownerJid,
4796
+ ownerJid: context.ownerJid,
4406
4797
  identifier: context.packKey,
4407
4798
  asset: { id: uploadedAssetId },
4408
4799
  emojis: [],
@@ -4411,7 +4802,7 @@ const handleManagedPackStickerCreateRequest = async (req, res, packKey) => {
4411
4802
 
4412
4803
  if (payload?.set_cover === true) {
4413
4804
  updatedPack = await stickerPackService.setPackCover({
4414
- ownerJid: context.session.ownerJid,
4805
+ ownerJid: context.ownerJid,
4415
4806
  identifier: context.packKey,
4416
4807
  stickerId: uploadedAssetId,
4417
4808
  });
@@ -4490,7 +4881,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4490
4881
  let uploadedAssetId = '';
4491
4882
  try {
4492
4883
  const originalPack = await stickerPackService.getPackInfo({
4493
- ownerJid: context.session.ownerJid,
4884
+ ownerJid: context.ownerJid,
4494
4885
  identifier: context.packKey,
4495
4886
  });
4496
4887
  const originalItems = Array.isArray(originalPack?.items) ? originalPack.items : [];
@@ -4504,12 +4895,12 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4504
4895
  }
4505
4896
 
4506
4897
  const normalizedUpload = await convertUploadMediaToWebp({
4507
- ownerJid: context.session.ownerJid,
4898
+ ownerJid: context.ownerJid,
4508
4899
  buffer: decoded.buffer,
4509
4900
  mimetype: decoded.mimetype || 'image/webp',
4510
4901
  });
4511
4902
  const asset = await saveStickerAssetFromBuffer({
4512
- ownerJid: context.session.ownerJid,
4903
+ ownerJid: context.ownerJid,
4513
4904
  buffer: normalizedUpload.buffer,
4514
4905
  mimetype: normalizedUpload.mimetype || 'image/webp',
4515
4906
  });
@@ -4525,7 +4916,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4525
4916
  }
4526
4917
 
4527
4918
  const swapResult = await runSqlTransaction(async (connection) => {
4528
- const packRow = await findStickerPackByOwnerAndIdentifier(context.session.ownerJid, context.packKey, { connection });
4919
+ const packRow = await findStickerPackByOwnerAndIdentifier(context.ownerJid, context.packKey, { connection });
4529
4920
  if (!packRow) return { status: 'pack_missing' };
4530
4921
 
4531
4922
  const liveOldItem = await getStickerPackItemByStickerId(packRow.id, normalizedStickerId, connection);
@@ -4577,7 +4968,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4577
4968
 
4578
4969
  if (swapResult?.status === 'old_sticker_missing') {
4579
4970
  const fresh = await stickerPackService
4580
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
4971
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4581
4972
  .catch(() => originalPack);
4582
4973
  await cleanupOrphanStickerAssets(uploadedAssetId ? [uploadedAssetId] : [], { reason: 'replace_sticker_old_missing' });
4583
4974
  await sendManagedPackMutationStatus(req, res, 'already_deleted', fresh, {
@@ -4589,7 +4980,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4589
4980
 
4590
4981
  if (swapResult?.status === 'duplicate_target') {
4591
4982
  const fresh = await stickerPackService
4592
- .getPackInfo({ ownerJid: context.session.ownerJid, identifier: context.packKey })
4983
+ .getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey })
4593
4984
  .catch(() => originalPack);
4594
4985
  await sendManagedPackMutationStatus(req, res, 'noop', fresh, {
4595
4986
  pack_key: context.packKey,
@@ -4602,7 +4993,7 @@ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, sticker
4602
4993
 
4603
4994
  invalidateStickerCatalogDerivedCaches();
4604
4995
  const finalPack = await stickerPackService.getPackInfo({
4605
- ownerJid: context.session.ownerJid,
4996
+ ownerJid: context.ownerJid,
4606
4997
  identifier: context.packKey,
4607
4998
  });
4608
4999
  await cleanupOrphanStickerAssets([normalizedStickerId], { reason: 'replace_sticker_old_cleanup' });
@@ -4715,55 +5106,37 @@ const handleCreatePackRequest = async (req, res) => {
4715
5106
  const manualTags = mergeUniqueTags(Array.isArray(payload?.tags) ? payload.tags : []).slice(0, 8);
4716
5107
  const persistedDescription = buildPackDescriptionWithTags(description, manualTags);
4717
5108
  const visibility = String(payload?.visibility || 'public').trim().toLowerCase();
4718
- const explicitOwnerJid = toOwnerJid(payload?.owner_jid);
4719
5109
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
4720
- let googleCreator = null;
5110
+ if (!googleSession?.ownerJid || !googleSession?.sub) {
5111
+ sendJson(req, res, 401, {
5112
+ error: 'Sessão expirada ou ausente. Faça login novamente para criar packs.',
5113
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
5114
+ });
5115
+ return;
5116
+ }
4721
5117
 
4722
- if (googleSession) {
4723
- googleCreator = {
4724
- ownerJid: googleSession.ownerJid,
5118
+ try {
5119
+ await assertGoogleIdentityNotBanned({
4725
5120
  sub: googleSession.sub,
4726
5121
  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,
5122
+ ownerJid: googleSession.ownerJid,
5123
+ });
5124
+ } catch (error) {
5125
+ sendJson(req, res, Number(error?.statusCode || 403), {
5126
+ error: error?.message || 'Conta sem permissão para criar packs.',
5127
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
4763
5128
  });
4764
5129
  return;
4765
5130
  }
4766
5131
 
5132
+ const googleCreator = {
5133
+ ownerJid: googleSession.ownerJid,
5134
+ sub: googleSession.sub,
5135
+ email: googleSession.email,
5136
+ name: googleSession.name,
5137
+ picture: googleSession.picture,
5138
+ };
5139
+
4767
5140
  if (googleCreator?.sub && googleCreator?.ownerJid) {
4768
5141
  await upsertGoogleWebUserRecord({
4769
5142
  sub: googleCreator.sub,
@@ -4779,17 +5152,7 @@ const handleCreatePackRequest = async (req, res) => {
4779
5152
  });
4780
5153
  }
4781
5154
 
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
- }
5155
+ const ownerJid = googleCreator.ownerJid;
4793
5156
 
4794
5157
  try {
4795
5158
  logPackWebFlow('info', 'create_pack_start', {
@@ -5581,6 +5944,7 @@ const buildSystemSummarySnapshot = async () => {
5581
5944
 
5582
5945
  const socketReadyState = Number(activeSocket?.ws?.readyState);
5583
5946
  const botJid = resolveBotJid(activeSocket?.user?.id) || null;
5947
+ const botPhone = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '') || null;
5584
5948
  const botConnected = Boolean(botJid) && socketReadyState === 1;
5585
5949
  const botConnectionStatus = botConnected
5586
5950
  ? 'online'
@@ -5646,6 +6010,7 @@ const buildSystemSummarySnapshot = async () => {
5646
6010
  connected: botConnected,
5647
6011
  connection_status: botConnectionStatus,
5648
6012
  jid: botJid,
6013
+ phone: botPhone,
5649
6014
  ready_state: Number.isFinite(socketReadyState) ? socketReadyState : null,
5650
6015
  },
5651
6016
  platform,
@@ -6510,6 +6875,15 @@ const handleSupportInfoRequest = async (req, res) => {
6510
6875
  sendJson(req, res, 200, { data });
6511
6876
  };
6512
6877
 
6878
+ const handleBotContactInfoRequest = async (req, res) => {
6879
+ const data = buildBotContactInfo();
6880
+ if (!data) {
6881
+ sendJson(req, res, 404, { error: 'Contato do bot indisponivel no momento.' });
6882
+ return;
6883
+ }
6884
+ sendJson(req, res, 200, { data });
6885
+ };
6886
+
6513
6887
  const handlePublicDataAssetRequest = async (req, res, pathname) => {
6514
6888
  const suffix = pathname.slice(STICKER_DATA_PUBLIC_PATH.length).replace(/^\/+/, '');
6515
6889
  if (!suffix) {
@@ -6759,7 +7133,7 @@ const handlePackInteractionRequest = async (req, res, packKey, interaction, url)
6759
7133
  const listAdminActiveGoogleWebSessions = async ({ limit = 200 } = {}) => {
6760
7134
  const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
6761
7135
  const rows = await executeQuery(
6762
- `SELECT session_token, google_sub, owner_jid, email, name, picture_url, created_at, last_seen_at, expires_at
7136
+ `SELECT session_token, google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, last_seen_at, expires_at
6763
7137
  FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
6764
7138
  WHERE revoked_at IS NULL
6765
7139
  AND expires_at > UTC_TIMESTAMP()
@@ -6770,6 +7144,7 @@ const listAdminActiveGoogleWebSessions = async ({ limit = 200 } = {}) => {
6770
7144
  session_token: String(row.session_token || '').trim(),
6771
7145
  google_sub: normalizeGoogleSubject(row.google_sub),
6772
7146
  owner_jid: normalizeJid(row.owner_jid) || null,
7147
+ owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
6773
7148
  email: normalizeEmail(row.email) || null,
6774
7149
  name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
6775
7150
  picture: String(row.picture_url || '').trim() || null,
@@ -6782,7 +7157,7 @@ const listAdminActiveGoogleWebSessions = async ({ limit = 200 } = {}) => {
6782
7157
  const listAdminKnownGoogleUsers = async ({ limit = 200 } = {}) => {
6783
7158
  const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
6784
7159
  const rows = await executeQuery(
6785
- `SELECT google_sub, owner_jid, email, name, picture_url, created_at, updated_at, last_login_at, last_seen_at
7160
+ `SELECT google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, updated_at, last_login_at, last_seen_at
6786
7161
  FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
6787
7162
  ORDER BY COALESCE(last_seen_at, last_login_at, updated_at, created_at) DESC
6788
7163
  LIMIT ${safeLimit}`,
@@ -6790,6 +7165,7 @@ const listAdminKnownGoogleUsers = async ({ limit = 200 } = {}) => {
6790
7165
  return (Array.isArray(rows) ? rows : []).map((row) => ({
6791
7166
  google_sub: normalizeGoogleSubject(row.google_sub),
6792
7167
  owner_jid: normalizeJid(row.owner_jid) || null,
7168
+ owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
6793
7169
  email: normalizeEmail(row.email) || null,
6794
7170
  name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
6795
7171
  picture: String(row.picture_url || '').trim() || null,
@@ -7307,7 +7683,7 @@ const handleCatalogApiRequest = async (req, res, pathname, url) => {
7307
7683
  }
7308
7684
 
7309
7685
  if (pathname === `${STICKER_API_BASE_PATH}/me`) {
7310
- await handleMyProfileRequest(req, res);
7686
+ await handleMyProfileRequest(req, res, url);
7311
7687
  return true;
7312
7688
  }
7313
7689
 
@@ -7453,6 +7829,15 @@ const handleCatalogApiRequest = async (req, res, pathname, url) => {
7453
7829
  return true;
7454
7830
  }
7455
7831
 
7832
+ if (segments.length === 1 && segments[0] === 'bot-contact') {
7833
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
7834
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7835
+ return true;
7836
+ }
7837
+ await handleBotContactInfoRequest(req, res);
7838
+ return true;
7839
+ }
7840
+
7456
7841
  if (segments[0] === 'admin') {
7457
7842
  if (segments.length === 2 && segments[1] === 'overview') {
7458
7843
  await handleAdminOverviewRequest(req, res);
@@ -7624,6 +8009,20 @@ const handleCatalogPageRequest = async (req, res, pathname) => {
7624
8009
 
7625
8010
  if (normalizedPath === STICKER_CREATE_WEB_PATH) {
7626
8011
  try {
8012
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
8013
+ if (!googleSession?.ownerJid) {
8014
+ const requestUrl = new URL(req.url || `${STICKER_CREATE_WEB_PATH}/`, SITE_ORIGIN);
8015
+ const nextPath = `${requestUrl.pathname}${requestUrl.search}`;
8016
+ const loginRedirectUrl = new URL(`${STICKER_LOGIN_WEB_PATH}/`, SITE_ORIGIN);
8017
+ loginRedirectUrl.searchParams.set('next', nextPath);
8018
+
8019
+ res.statusCode = 302;
8020
+ res.setHeader('Location', `${loginRedirectUrl.pathname}${loginRedirectUrl.search}`);
8021
+ res.setHeader('Cache-Control', 'no-store');
8022
+ res.end();
8023
+ return;
8024
+ }
8025
+
7627
8026
  const html = await renderCreatePackHtml();
7628
8027
  sendText(req, res, 200, html, 'text/html; charset=utf-8');
7629
8028
  return;
@@ -7711,6 +8110,7 @@ export const isStickerCatalogEnabled = () => STICKER_CATALOG_ENABLED;
7711
8110
  export const getStickerCatalogConfig = () => ({
7712
8111
  enabled: STICKER_CATALOG_ENABLED,
7713
8112
  webPath: STICKER_WEB_PATH,
8113
+ userProfilePath: USER_PROFILE_WEB_PATH,
7714
8114
  apiBasePath: STICKER_API_BASE_PATH,
7715
8115
  orphanApiPath: STICKER_ORPHAN_API_PATH,
7716
8116
  dataPublicPath: STICKER_DATA_PUBLIC_PATH,
@@ -7734,6 +8134,29 @@ export async function maybeHandleStickerCatalogRequest(req, res, { pathname, url
7734
8134
  if (!['GET', 'HEAD', 'POST', 'PATCH', 'DELETE'].includes(req.method || '')) return false;
7735
8135
  if (maybeRedirectToCanonicalHost(req, res, url)) return true;
7736
8136
 
8137
+ if (pathname === USER_PROFILE_WEB_PATH || pathname === `${USER_PROFILE_WEB_PATH}/`) {
8138
+ if (!['GET', 'HEAD'].includes(req.method || '')) return false;
8139
+ try {
8140
+ const html = await renderUserDashboardHtml();
8141
+ res.setHeader('Cache-Control', 'no-store');
8142
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
8143
+ sendText(req, res, 200, html, 'text/html; charset=utf-8');
8144
+ } catch (error) {
8145
+ if (error?.code === 'ENOENT') {
8146
+ sendJson(req, res, 404, { error: 'Template da pagina de usuario nao encontrado.' });
8147
+ return true;
8148
+ }
8149
+
8150
+ logger.error('Falha ao renderizar pagina de usuario.', {
8151
+ action: 'user_dashboard_page_render_failed',
8152
+ path: pathname,
8153
+ error: error?.message,
8154
+ });
8155
+ sendJson(req, res, 500, { error: 'Falha interna ao renderizar pagina de usuario.' });
8156
+ }
8157
+ return true;
8158
+ }
8159
+
7737
8160
  if (pathname === '/sitemap.xml') {
7738
8161
  if (!['GET', 'HEAD'].includes(req.method || '')) return false;
7739
8162
  try {