From 1a388a25934c52ae04e83d757d0f2f486ca7da5d Mon Sep 17 00:00:00 2001 From: ziad Date: Sun, 12 Jul 2026 03:58:07 +1000 Subject: [PATCH] Convert Telegram HEIC uploads to JPEG before Supabase storage. EXIF saleStart is still read from the original buffer in createMomentsFromGroup; only the stored asset is normalized to image/jpeg so downstream consumers avoid HEIC decoding. Co-authored-by: Cursor --- .../uploadPhotoAttachment.heic.test.ts | 69 +++++++++++++++++++ .../telegram/chat/uploadPhotoAttachment.ts | 13 +++- 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/lib/telegram/chat/__tests__/uploadPhotoAttachment.heic.test.ts diff --git a/src/lib/telegram/chat/__tests__/uploadPhotoAttachment.heic.test.ts b/src/lib/telegram/chat/__tests__/uploadPhotoAttachment.heic.test.ts new file mode 100644 index 00000000..297b7f17 --- /dev/null +++ b/src/lib/telegram/chat/__tests__/uploadPhotoAttachment.heic.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import uploadPhotoAttachment from '../uploadPhotoAttachment'; + +vi.mock('@/lib/supabase/storage/uploadFileToSupabase', () => ({ + default: vi.fn(), +})); +vi.mock('@/lib/supabase/storage/uploadJsonToSupabase', () => ({ + default: vi.fn(), +})); +vi.mock('../getTelegramFilePath', () => ({ default: vi.fn() })); + +import uploadFileToSupabase from '@/lib/supabase/storage/uploadFileToSupabase'; +import uploadJsonToSupabase from '@/lib/supabase/storage/uploadJsonToSupabase'; +import getTelegramFilePath from '../getTelegramFilePath'; + +const PHOTO_URL = + 'https://supabase.co/storage/v1/object/public/bucket/photo.jpg'; +const META_URL = + 'https://supabase.co/storage/v1/object/public/bucket/meta.json'; + +const readHeicFixture = (name: string): Buffer => + readFileSync(join(__dirname, 'fixtures', name)); + +const HEIC_DECODE_TIMEOUT_MS = 30_000; + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getTelegramFilePath).mockResolvedValue('photos/file_1'); + vi.mocked(uploadFileToSupabase).mockResolvedValue(PHOTO_URL); + vi.mocked(uploadJsonToSupabase).mockResolvedValue(META_URL); +}); + +describe('uploadPhotoAttachment HEIC integration', () => { + it( + 'uploads HEIC attachments as JPEG', + async () => { + const heicBuffer = readHeicFixture('apple-styled-photo.heic'); + + const result = await uploadPhotoAttachment( + { + type: 'image', + mimeType: 'image/heic', + fetchData: () => Promise.resolve(heicBuffer), + } as never, + 'file-id', + 'My HEIC Photo' + ); + + expect(result.mimeType).toBe('image/jpeg'); + expect(uploadFileToSupabase).toHaveBeenCalledOnce(); + + const [file] = vi.mocked(uploadFileToSupabase).mock.calls[0]; + expect(file.type).toBe('image/jpeg'); + + const uploaded = Buffer.from(await file.arrayBuffer()); + expect(uploaded[0]).toBe(0xff); + expect(uploaded[1]).toBe(0xd8); + + expect(uploadJsonToSupabase).toHaveBeenCalledWith({ + name: 'My HEIC Photo', + image: PHOTO_URL, + content: { mime: 'image/jpeg', uri: PHOTO_URL }, + }); + }, + HEIC_DECODE_TIMEOUT_MS + ); +}); diff --git a/src/lib/telegram/chat/uploadPhotoAttachment.ts b/src/lib/telegram/chat/uploadPhotoAttachment.ts index 63d2d2c6..d9c875cd 100644 --- a/src/lib/telegram/chat/uploadPhotoAttachment.ts +++ b/src/lib/telegram/chat/uploadPhotoAttachment.ts @@ -1,6 +1,8 @@ import type { Attachment } from 'chat'; import uploadFileToSupabase from '@/lib/supabase/storage/uploadFileToSupabase'; import uploadJsonToSupabase from '@/lib/supabase/storage/uploadJsonToSupabase'; +import prepareImageBufferForSharp from '@/lib/media/prepareImageBufferForSharp'; +import isHeicBuffer from '@/lib/media/isHeicBuffer'; import getTelegramFilePath from './getTelegramFilePath'; import getMimeTypeFromFilePath from './getMimeTypeFromFilePath'; import toFile from './toFile'; @@ -16,8 +18,15 @@ const uploadPhotoAttachment = async ( attachment.fetchData(), getTelegramFilePath(fileId), ]); - const mimeType = attachment.mimeType ?? getMimeTypeFromFilePath(filePath); - const photoFile = toFile(buffer, name, mimeType); + let mimeType = attachment.mimeType ?? getMimeTypeFromFilePath(filePath); + let uploadBuffer = buffer; + + if (isHeicBuffer(buffer)) { + uploadBuffer = await prepareImageBufferForSharp(buffer); + mimeType = 'image/jpeg'; + } + + const photoFile = toFile(uploadBuffer, name, mimeType); const photoUri = await uploadFileToSupabase(photoFile); const jsonObject = {