Move avatar profile resolution into telegram backend

This commit is contained in:
Ruslan Bakiev
2026-03-12 18:44:46 +07:00
parent 6d1bface33
commit 3fc9e44e23
2 changed files with 74 additions and 3 deletions

View File

@@ -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<Array<{ file_id?: string }>>;
};
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<OmniInboundEnvelopeV1> {
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,
},
};
}