diff --git a/frontend b/frontend index b01072f..3518c80 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit b01072f52f41be7213a20b02ac51242d0c6d1be5 +Subproject commit 3518c80b92f0c95b2ace564f7eeca3fe7d053937 diff --git a/telegram_backend/src/profile-state.ts b/telegram_backend/src/profile-state.ts index 51540d1..95aeeb8 100644 --- a/telegram_backend/src/profile-state.ts +++ b/telegram_backend/src/profile-state.ts @@ -2,6 +2,7 @@ import { Pool } from "pg"; import type { OmniInboundEnvelopeV1 } from "./types"; const TELEGRAM_FILE_MARKER = "tg-file:"; +const TELEGRAM_API_BASE_DEFAULT = "https://api.telegram.org"; type AvatarStateRow = { avatar_fingerprint: string | null; @@ -28,6 +29,64 @@ function parseTelegramFileId(avatarUrl: string | null) { return fileId || null; } +function parseTelegramUserId(value: string | null) { + const raw = asString(value); + if (!raw || !/^\d+$/.test(raw)) return null; + const userId = Number.parseInt(raw, 10); + if (!Number.isFinite(userId) || userId <= 0) return null; + return userId; +} + +function telegramApiBase() { + return asString(process.env.TELEGRAM_API_BASE) ?? TELEGRAM_API_BASE_DEFAULT; +} + +type TelegramUserProfilePhotosResponse = { + ok?: boolean; + result?: { + photos?: Array>; + }; + description?: string; +}; + +function pickProfilePhotoFileId(payload: TelegramUserProfilePhotosResponse) { + const groups = payload.result?.photos; + if (!Array.isArray(groups) || !groups.length) return null; + const firstGroup = groups[0]; + if (!Array.isArray(firstGroup) || !firstGroup.length) return null; + + for (let index = firstGroup.length - 1; index >= 0; index -= 1) { + const fileId = asString(firstGroup[index]?.file_id); + if (fileId) return fileId; + } + + return null; +} + +async function resolveProfileAvatarFileId(contactExternalId: string | null) { + const userId = parseTelegramUserId(contactExternalId); + if (!userId) return null; + + const token = asString(process.env.TELEGRAM_BOT_TOKEN); + if (!token) return null; + + const response = await fetch(`${telegramApiBase().replace(/\/+$/, "")}/bot${token}/getUserProfilePhotos`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + user_id: userId, + offset: 0, + limit: 1, + }), + }); + + if (!response.ok) return null; + const payload = (await response.json()) as TelegramUserProfilePhotosResponse; + if (!payload.ok) return null; + + return pickProfilePhotoFileId(payload); +} + const pool = new Pool({ connectionString: requiredEnv("TELEGRAM_PROFILE_STATE_DATABASE_URL"), }); @@ -101,8 +160,19 @@ async function detectAvatarChange(input: { export async function applyAvatarProfileState(envelope: OmniInboundEnvelopeV1): Promise { const payload = envelope.payloadNormalized; const contactExternalId = asString(payload.contactExternalId); - const contactAvatarUrl = asString(payload.contactAvatarUrl); - const contactAvatarFingerprint = asString(payload.contactAvatarFingerprint); + let contactAvatarUrl = asString(payload.contactAvatarUrl); + let contactAvatarFingerprint = asString(payload.contactAvatarFingerprint); + + if (!contactAvatarUrl) { + const profileFileId = await resolveProfileAvatarFileId(contactExternalId); + if (profileFileId) { + contactAvatarUrl = `${TELEGRAM_FILE_MARKER}${profileFileId}`; + if (!contactAvatarFingerprint) { + contactAvatarFingerprint = profileFileId; + } + } + } + const avatarFileId = parseTelegramFileId(contactAvatarUrl); const avatarState = await detectAvatarChange({ @@ -117,6 +187,7 @@ export async function applyAvatarProfileState(envelope: OmniInboundEnvelopeV1): ...payload, contactAvatarChanged: avatarState.changed, contactAvatarUrl: avatarState.changed ? contactAvatarUrl : null, + contactAvatarFingerprint, }, }; }