From 651572f15fc3392d188d12ec023c6716b15783cc Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Fri, 24 Apr 2026 21:31:58 +0300 Subject: [PATCH] refactor: show tg avatar in detached browser --- bot/app/web/subscription_webapp.py | 15 ++++++ bot/app/web/templates/subscription_webapp.js | 48 ++++++++++++++------ db/dal/user_dal.py | 2 +- db/migrator.py | 17 +++++++ db/models.py | 1 + 5 files changed, 68 insertions(+), 15 deletions(-) diff --git a/bot/app/web/subscription_webapp.py b/bot/app/web/subscription_webapp.py index 33e78ba..cad0e78 100644 --- a/bot/app/web/subscription_webapp.py +++ b/bot/app/web/subscription_webapp.py @@ -854,6 +854,14 @@ async def _sync_panel_identity_for_user(request: web.Request, user: User) -> Non ) +def _telegram_photo_url_value(telegram_user: Dict[str, Any]) -> Optional[str]: + raw_value = telegram_user.get("photo_url") + if not raw_value: + return None + value = str(raw_value).strip() + return value or None + + def _apply_telegram_profile_to_user( user: User, telegram_user: Dict[str, Any], @@ -868,6 +876,9 @@ def _apply_telegram_profile_to_user( user.first_name = sanitize_display_name(telegram_user.get("first_name")) user.last_name = sanitize_display_name(telegram_user.get("last_name")) user.language_code = language_code + telegram_photo_url = _telegram_photo_url_value(telegram_user) + if telegram_photo_url: + user.telegram_photo_url = telegram_photo_url async def _link_telegram_to_user( @@ -1067,6 +1078,9 @@ async def _ensure_user_from_telegram( "last_name": sanitize_display_name(telegram_user.get("last_name")), "language_code": language_code, } + telegram_photo_url = _telegram_photo_url_value(telegram_user) + if telegram_photo_url: + update_data["telegram_photo_url"] = telegram_photo_url db_user = await user_dal.get_user_by_telegram_id(session, user_id) if not db_user: @@ -1153,6 +1167,7 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A "email_verified": bool(db_user.email_verified_at), "telegram_id": db_user.telegram_id, "telegram_linked": bool(_telegram_id_for_user(db_user)), + "telegram_photo_url": db_user.telegram_photo_url, "first_name": db_user.first_name, "language_code": lang, }, diff --git a/bot/app/web/templates/subscription_webapp.js b/bot/app/web/templates/subscription_webapp.js index 7c70cf1..44eea40 100644 --- a/bot/app/web/templates/subscription_webapp.js +++ b/bot/app/web/templates/subscription_webapp.js @@ -615,7 +615,7 @@ const MOCK = (() => { script.src = TELEGRAM_LOGIN_WIDGET_URL; script.setAttribute('data-telegram-login', botUsername); script.setAttribute('data-size', 'large'); - script.setAttribute('data-userpic', 'false'); + script.setAttribute('data-userpic', 'true'); script.setAttribute('data-request-access', 'write'); script.setAttribute('data-onauth', 'onTelegramAuth(user)'); script.onerror = () => setAuthStatus(t('telegram_auth_unavailable'), true); @@ -1003,7 +1003,8 @@ const MOCK = (() => { const telegramLinked = Boolean(user.telegram_linked); const displayName = getUserDisplayName(user); const secondary = getUserSecondaryText(user, emailLinked, telegramLinked); - const avatar = getUserAvatarSrc(user, emailLinked); + const avatarSrc = getUserAvatarSrc(user); + const avatarFallback = buildIdenticon(getUserAvatarSeed(user, emailLinked)); const chipName = document.getElementById('user-chip-name'); const chipAvatar = document.getElementById('user-chip-avatar'); @@ -1014,10 +1015,8 @@ const MOCK = (() => { const telegramStatus = document.getElementById('user-dropdown-telegram-status'); chipName.textContent = displayName; - chipAvatar.src = avatar; - chipAvatar.alt = displayName; - dropAvatar.src = avatar; - dropAvatar.alt = displayName; + setAvatarWithFallback(chipAvatar, avatarSrc, avatarFallback, displayName); + setAvatarWithFallback(dropAvatar, avatarSrc, avatarFallback, displayName); dropName.textContent = displayName; dropSub.textContent = secondary; dropSub.classList.toggle('hidden', !secondary); @@ -1059,6 +1058,19 @@ const MOCK = (() => { return tg.initDataUnsafe.user; } + function getUserAvatarSeed(user, emailLinked) { + const tgUser = getTelegramInitUser(); + if (emailLinked && user && user.email) return user.email.trim().toLowerCase(); + return String((user && user.id) || (user && user.telegram_id) || (tgUser && tgUser.id) || 'guest'); + } + + function getUserAvatarSrc(user) { + const tgUser = getTelegramInitUser(); + if (tgUser && tgUser.photo_url) return tgUser.photo_url; + if (user && user.telegram_photo_url) return user.telegram_photo_url; + return ''; + } + function getUserDisplayName(user) { const tgUser = getTelegramInitUser(); if (tgUser && tgUser.first_name) return tgUser.first_name; @@ -1075,13 +1087,21 @@ const MOCK = (() => { return ''; } - function getUserAvatarSrc(user, emailLinked) { - const tgUser = getTelegramInitUser(); - if (tgUser && tgUser.photo_url) return tgUser.photo_url; - const seed = (emailLinked && user.email) - ? user.email.trim().toLowerCase() - : String(user.id || user.telegram_id || tgUser && tgUser.id || 'guest'); - return buildIdenticon(seed); + function setAvatarWithFallback(img, avatarSrc, avatarFallback, altText) { + if (!img) return; + + img.alt = altText || ''; + if (!avatarSrc) { + img.onerror = null; + img.src = avatarFallback; + return; + } + + img.onerror = () => { + img.onerror = null; + img.src = avatarFallback; + }; + img.src = avatarSrc; } function buildIdenticon(seed) { @@ -1460,7 +1480,7 @@ const MOCK = (() => { script.src = TELEGRAM_LOGIN_WIDGET_URL; script.setAttribute('data-telegram-login', botUsername); script.setAttribute('data-size', 'large'); - script.setAttribute('data-userpic', 'false'); + script.setAttribute('data-userpic', 'true'); script.setAttribute('data-request-access', 'write'); script.setAttribute('data-onauth', 'onTelegramLinkAuth(user)'); script.onerror = () => setTelegramLinkStatus(t('telegram_auth_unavailable'), true); diff --git a/db/dal/user_dal.py b/db/dal/user_dal.py index 565c517..d70918d 100644 --- a/db/dal/user_dal.py +++ b/db/dal/user_dal.py @@ -287,7 +287,7 @@ async def merge_users( if referral_code_to_move: target.referral_code = referral_code_to_move - for attr in ("username", "first_name", "last_name", "language_code"): + for attr in ("username", "first_name", "last_name", "language_code", "telegram_photo_url"): if not getattr(target, attr) and getattr(source, attr): setattr(target, attr, getattr(source, attr)) if not target.referred_by_id and source.referred_by_id != target_user_id: diff --git a/db/migrator.py b/db/migrator.py index e0062f2..e2cf15d 100644 --- a/db/migrator.py +++ b/db/migrator.py @@ -239,6 +239,18 @@ def _migration_0006_add_security_throttles(connection: Connection) -> None: ) ) + +def _migration_0007_add_telegram_photo_url(connection: Connection) -> None: + inspector = inspect(connection) + columns: Set[str] = {col["name"] for col in inspector.get_columns("users")} + if "telegram_photo_url" in columns: + return + + connection.execute( + text("ALTER TABLE users ADD COLUMN telegram_photo_url TEXT") + ) + + MIGRATIONS: List[Migration] = [ Migration( id="0001_add_channel_subscription_fields", @@ -270,6 +282,11 @@ MIGRATIONS: List[Migration] = [ description="Add generic lockout tracking for brute-force protection", upgrade=_migration_0006_add_security_throttles, ), + Migration( + id="0007_add_telegram_photo_url", + description="Store Telegram profile photo URLs for linked users", + upgrade=_migration_0007_add_telegram_photo_url, + ), ] diff --git a/db/models.py b/db/models.py index 9b7dda9..2f182ae 100644 --- a/db/models.py +++ b/db/models.py @@ -17,6 +17,7 @@ class User(Base): email = Column(String, nullable=True, unique=True, index=True) email_verified_at = Column(DateTime(timezone=True), nullable=True) telegram_id = Column(BigInteger, nullable=True, unique=True, index=True) + telegram_photo_url = Column(Text, nullable=True) first_name = Column(String, nullable=True) last_name = Column(String, nullable=True) language_code = Column(String, default="ru")