diff --git a/omni_chat/src/worker.ts b/omni_chat/src/worker.ts index dc56f1d..4d8836a 100644 --- a/omni_chat/src/worker.ts +++ b/omni_chat/src/worker.ts @@ -30,6 +30,7 @@ export const RECEIVER_FLOW_QUEUE_NAME = (process.env.RECEIVER_FLOW_QUEUE_NAME || const TELEGRAM_PLACEHOLDER_PREFIX = "Telegram "; const TELEGRAM_AUDIO_FILE_MARKER = "tg-file:"; const TELEGRAM_WAVE_BINS = 96; +const TELEGRAM_AVATAR_FILE_MARKER = "tg-file:"; function redisConnectionFromEnv(): ConnectionOptions { const raw = (process.env.REDIS_URL || "redis://localhost:6379").trim(); @@ -168,6 +169,69 @@ async function fetchTelegramFileBytes(fileId: string) { return new Uint8Array(await fileRes.arrayBuffer()); } +type TelegramChatPhoto = { + small_file_id?: string; + big_file_id?: string; +}; + +type TelegramGetChatResponse = { + ok?: boolean; + result?: { + photo?: TelegramChatPhoto; + }; +}; + +type TelegramProfilePhotoSize = { + file_id?: string; +}; + +type TelegramGetUserProfilePhotosResponse = { + ok?: boolean; + result?: { + photos?: TelegramProfilePhotoSize[][]; + }; +}; + +function asTelegramAvatarUrl(fileId: string | null | undefined) { + const normalized = asString(fileId); + if (!normalized) return null; + return `${TELEGRAM_AVATAR_FILE_MARKER}${normalized}`; +} + +async function fetchTelegramAvatarUrl(externalContactId: string) { + const token = String(process.env.TELEGRAM_BOT_TOKEN ?? "").trim(); + if (!token) return null; + + const base = String(process.env.TELEGRAM_API_BASE ?? "https://api.telegram.org").replace(/\/+$/, ""); + + const getChatRes = await fetch(`${base}/bot${token}/getChat`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ chat_id: externalContactId }), + }); + const getChatJson = (await getChatRes.json()) as TelegramGetChatResponse; + if (getChatRes.ok && getChatJson.ok) { + const fromChat = asTelegramAvatarUrl( + getChatJson.result?.photo?.small_file_id ?? getChatJson.result?.photo?.big_file_id, + ); + if (fromChat) return fromChat; + } + + const getPhotosRes = await fetch(`${base}/bot${token}/getUserProfilePhotos`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ user_id: externalContactId, limit: 1 }), + }); + const getPhotosJson = (await getPhotosRes.json()) as TelegramGetUserProfilePhotosResponse; + if (!getPhotosRes.ok || !getPhotosJson.ok) return null; + + const firstPhotoSizes = Array.isArray(getPhotosJson.result?.photos?.[0]) + ? getPhotosJson.result?.photos?.[0] + : []; + const candidate = firstPhotoSizes.at(-1)?.file_id ?? firstPhotoSizes[0]?.file_id; + return asTelegramAvatarUrl(candidate); +} + async function resolveInboundWaveform(media: TelegramInboundMedia, text: string) { const fallback = buildFallbackWaveform(`${media.fileId ?? "none"}:${media.durationSec ?? "0"}:${text}`); const fileId = media.fileId; @@ -232,7 +296,7 @@ function buildContactProfile( }; } -async function maybeHydrateContact(contactId: string, profile: ContactProfile) { +async function maybeHydrateContact(contactId: string, profile: ContactProfile, externalContactId: string) { const current = await prisma.contact.findUnique({ where: { id: contactId }, select: { name: true, avatarUrl: true }, @@ -248,8 +312,9 @@ async function maybeHydrateContact(contactId: string, profile: ContactProfile) { } const currentAvatar = asString(current.avatarUrl); - if (profile.avatarUrl && profile.avatarUrl !== currentAvatar) { - updates.avatarUrl = profile.avatarUrl; + const resolvedAvatarUrl = profile.avatarUrl ?? (currentAvatar ? null : await fetchTelegramAvatarUrl(externalContactId)); + if (resolvedAvatarUrl && resolvedAvatarUrl !== currentAvatar) { + updates.avatarUrl = resolvedAvatarUrl; } if (Object.keys(updates).length === 0) return; @@ -306,15 +371,17 @@ async function resolveContact(input: { select: { contactId: true }, }); if (existingIdentity?.contactId) { - await maybeHydrateContact(existingIdentity.contactId, input.profile); + await maybeHydrateContact(existingIdentity.contactId, input.profile, input.externalContactId); return existingIdentity.contactId; } + const avatarUrl = input.profile.avatarUrl ?? (await fetchTelegramAvatarUrl(input.externalContactId)); + const contact = await prisma.contact.create({ data: { teamId: input.teamId, name: input.profile.displayName, - avatarUrl: input.profile.avatarUrl, + avatarUrl, }, select: { id: true }, }); @@ -342,7 +409,7 @@ async function resolveContact(input: { if (!concurrentIdentity?.contactId) throw error; await prisma.contact.delete({ where: { id: contact.id } }).catch(() => undefined); - await maybeHydrateContact(concurrentIdentity.contactId, input.profile); + await maybeHydrateContact(concurrentIdentity.contactId, input.profile, input.externalContactId); return concurrentIdentity.contactId; }