Fix Telegram contact avatars in CRM list
This commit is contained in:
75
frontend/server/api/omni/telegram/avatar.get.ts
Normal file
75
frontend/server/api/omni/telegram/avatar.get.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { getQuery, setHeader } from "h3";
|
||||||
|
import { getAuthContext } from "../../../utils/auth";
|
||||||
|
import { prisma } from "../../../utils/prisma";
|
||||||
|
import { requireTelegramBotToken, telegramApiBase, telegramBotApi } from "../../../utils/telegram";
|
||||||
|
|
||||||
|
const TELEGRAM_FILE_MARKER = "tg-file:";
|
||||||
|
|
||||||
|
type TelegramFileMeta = {
|
||||||
|
file_id: string;
|
||||||
|
file_path?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseTelegramFileId(avatarUrl: string | null | undefined) {
|
||||||
|
const raw = String(avatarUrl ?? "").trim();
|
||||||
|
if (!raw.startsWith(TELEGRAM_FILE_MARKER)) return null;
|
||||||
|
const fileId = raw.slice(TELEGRAM_FILE_MARKER.length).trim();
|
||||||
|
return fileId || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const auth = await getAuthContext(event);
|
||||||
|
const query = getQuery(event);
|
||||||
|
const contactId = String(query.contactId ?? "").trim();
|
||||||
|
|
||||||
|
if (!contactId) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: "contactId is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const contact = await prisma.contact.findFirst({
|
||||||
|
where: {
|
||||||
|
id: contactId,
|
||||||
|
teamId: auth.teamId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
avatarUrl: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!contact) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: "contact not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileId = parseTelegramFileId(contact.avatarUrl);
|
||||||
|
if (!fileId) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: "telegram avatar is missing" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = await telegramBotApi<TelegramFileMeta>("getFile", { file_id: fileId });
|
||||||
|
const filePath = String(meta?.file_path ?? "").trim();
|
||||||
|
if (!filePath) {
|
||||||
|
throw createError({ statusCode: 502, statusMessage: "telegram file path is unavailable" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiBase = telegramApiBase().replace(/\/+$/, "");
|
||||||
|
const token = requireTelegramBotToken();
|
||||||
|
const remote = await fetch(`${apiBase}/file/bot${token}/${filePath}`);
|
||||||
|
if (!remote.ok) {
|
||||||
|
throw createError({ statusCode: 502, statusMessage: `telegram file fetch failed (${remote.status})` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = remote.headers.get("content-type") || "image/jpeg";
|
||||||
|
const contentLength = remote.headers.get("content-length");
|
||||||
|
const body = Buffer.from(await remote.arrayBuffer());
|
||||||
|
|
||||||
|
setHeader(event, "content-type", contentType);
|
||||||
|
setHeader(event, "cache-control", "private, max-age=300");
|
||||||
|
if (contentLength) {
|
||||||
|
const parsedLength = Number(contentLength);
|
||||||
|
if (Number.isFinite(parsedLength) && parsedLength >= 0) {
|
||||||
|
setHeader(event, "content-length", parsedLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
});
|
||||||
@@ -120,6 +120,18 @@ function resolveContactMessageAudioUrl(message: {
|
|||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveContactAvatarUrl(contact: {
|
||||||
|
id: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
}) {
|
||||||
|
const raw = String(contact.avatarUrl ?? "").trim();
|
||||||
|
if (!raw) return "";
|
||||||
|
if (raw.startsWith(TELEGRAM_AUDIO_FILE_MARKER)) {
|
||||||
|
return `/api/omni/telegram/avatar?contactId=${encodeURIComponent(contact.id)}`;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
async function upsertContactInbox(input: {
|
async function upsertContactInbox(input: {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
contactId: string;
|
contactId: string;
|
||||||
@@ -484,7 +496,7 @@ async function getContacts(auth: AuthContext | null) {
|
|||||||
.map((c) => ({
|
.map((c) => ({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
name: c.name,
|
name: c.name,
|
||||||
avatar: c.avatarUrl ?? "",
|
avatar: resolveContactAvatarUrl(c),
|
||||||
channels: Array.from(channelsByContactId.get(c.id) ?? []),
|
channels: Array.from(channelsByContactId.get(c.id) ?? []),
|
||||||
lastContactAt: c.messages[0]?.occurredAt?.toISOString?.() ?? c.updatedAt.toISOString(),
|
lastContactAt: c.messages[0]?.occurredAt?.toISOString?.() ?? c.updatedAt.toISOString(),
|
||||||
lastMessageText: c.messages[0]?.content ?? "",
|
lastMessageText: c.messages[0]?.content ?? "",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|||||||
import type { JsonObject, OmniInboundEnvelopeV1 } from "./types";
|
import type { JsonObject, OmniInboundEnvelopeV1 } from "./types";
|
||||||
|
|
||||||
const MAX_TEXT_LENGTH = 4096;
|
const MAX_TEXT_LENGTH = 4096;
|
||||||
|
const TELEGRAM_FILE_MARKER = "tg-file:";
|
||||||
|
|
||||||
function asObject(value: unknown): JsonObject {
|
function asObject(value: unknown): JsonObject {
|
||||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : {};
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : {};
|
||||||
@@ -67,6 +68,11 @@ function normalizeId(value: unknown) {
|
|||||||
return text || null;
|
return text || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickTelegramChatPhotoFileId(source: JsonObject | null | undefined) {
|
||||||
|
const photo = asObject(asObject(source).photo);
|
||||||
|
return normalizeString(photo.small_file_id) ?? normalizeString(photo.big_file_id);
|
||||||
|
}
|
||||||
|
|
||||||
type TelegramMediaInfo = {
|
type TelegramMediaInfo = {
|
||||||
kind: "voice" | "audio" | "video_note" | null;
|
kind: "voice" | "audio" | "video_note" | null;
|
||||||
fileId: string | null;
|
fileId: string | null;
|
||||||
@@ -219,6 +225,7 @@ export function parseTelegramBusinessUpdate(raw: unknown): OmniInboundEnvelopeV1
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fallbackContactSource = contactSource === chat ? from : chat;
|
const fallbackContactSource = contactSource === chat ? from : chat;
|
||||||
|
const contactAvatarFileId = pickTelegramChatPhotoFileId(contactSource);
|
||||||
|
|
||||||
const threadExternalId =
|
const threadExternalId =
|
||||||
chat.id != null
|
chat.id != null
|
||||||
@@ -293,7 +300,7 @@ export function parseTelegramBusinessUpdate(raw: unknown): OmniInboundEnvelopeV1
|
|||||||
contactFirstName: normalizeString(contactSource.first_name),
|
contactFirstName: normalizeString(contactSource.first_name),
|
||||||
contactLastName: normalizeString(contactSource.last_name),
|
contactLastName: normalizeString(contactSource.last_name),
|
||||||
contactTitle: normalizeString(contactSource.title),
|
contactTitle: normalizeString(contactSource.title),
|
||||||
contactAvatarUrl: normalizeString(contactSource.photo_url),
|
contactAvatarUrl: contactAvatarFileId ? `${TELEGRAM_FILE_MARKER}${contactAvatarFileId}` : null,
|
||||||
fromUsername: typeof from.username === "string" ? from.username : null,
|
fromUsername: typeof from.username === "string" ? from.username : null,
|
||||||
fromFirstName: typeof from.first_name === "string" ? from.first_name : null,
|
fromFirstName: typeof from.first_name === "string" ? from.first_name : null,
|
||||||
fromLastName: typeof from.last_name === "string" ? from.last_name : null,
|
fromLastName: typeof from.last_name === "string" ? from.last_name : null,
|
||||||
|
|||||||
Reference in New Issue
Block a user