Support HEIC EXIF timezone handling in the Telegram bot#403
Conversation
Parse capture-time offsets from OffsetTimeOriginal or OffsetTime, sniff HEIC MIME from magic bytes for extensionless documents, and patch exifr detection for Apple multi-brand HEIC files so saleStart uses the true UTC instant. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds HEIC/HEIF MIME detection from paths and file signatures, resolves extensionless Telegram attachments using downloaded bytes, patches HEIC EXIF parser detection, and supports EXIF timezone fallback from ChangesTelegram media resolution
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant onNewMention
participant resolveTelegramMediaAttachment
participant fetchTelegramFile
participant resolveTelegramFileMimeType
onNewMention->>resolveTelegramMediaAttachment: resolve message attachment
resolveTelegramMediaAttachment->>fetchTelegramFile: fetch Telegram file
fetchTelegramFile->>resolveTelegramFileMimeType: inspect path and buffer
resolveTelegramFileMimeType-->>fetchTelegramFile: resolved MIME type
fetchTelegramFile-->>resolveTelegramMediaAttachment: file metadata
resolveTelegramMediaAttachment-->>onNewMention: normalized attachment
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
src/lib/telegram/chat/__tests__/bufferStartsWith.test.tsOops! Something went wrong! :( ESLint: 9.39.4 TypeError: Converting circular structure to JSON ... [truncated 446 characters] ... c/dist/eslintrc.cjs:3261:25) src/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.tsOops! Something went wrong! :( ESLint: 9.39.4 TypeError: Converting circular structure to JSON ... [truncated 446 characters] ... c/dist/eslintrc.cjs:3261:25) src/lib/telegram/chat/__tests__/extractExifCaptureDate.heic.test.tsOops! Something went wrong! :( ESLint: 9.39.4 TypeError: Converting circular structure to JSON ... [truncated 446 characters] ... c/dist/eslintrc.cjs:3261:25)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/lib/telegram/chat/bufferStartsWith.ts (1)
1-2: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueLGTM!
Optionally, add an explicit length guard so the function doesn't rely on Buffer returning
undefinedfor out-of-bounds indices:♻️ Optional robustness improvement
const bufferStartsWith = (buffer: Buffer, magic: number[]): boolean => - magic.every((byte, index) => buffer[index] === byte); + buffer.length >= magic.length && + magic.every((byte, index) => buffer[index] === byte);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/telegram/chat/bufferStartsWith.ts` around lines 1 - 2, Optionally update bufferStartsWith to first verify that buffer is at least as long as magic, returning false when it is shorter, then preserve the existing byte-by-byte comparison for valid lengths.src/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.ts (1)
1-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd tests for JPEG, PNG, GIF, WEBP, and video/mp4 detection.
Only HEIC and unknown-byte cases are tested. The remaining MIME branches in
detectMimeTypeFromBuffer(JPEG, PNG, GIF, WEBP, ftyp-based video/mp4) have no coverage. These can be tested with small crafted buffers.♻️ Suggested additional tests
it('detects JPEG from magic bytes', () => { expect(detectMimeTypeFromBuffer(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00]))) .toBe('image/jpeg'); }); it('detects PNG from magic bytes', () => { const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); expect(detectMimeTypeFromBuffer(png)).toBe('image/png'); }); it('detects GIF from magic bytes', () => { const gif = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x00]); expect(detectMimeTypeFromBuffer(gif)).toBe('image/gif'); }); it('detects WEBP from magic bytes', () => { const webp = Buffer.from([ 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, ]); expect(detectMimeTypeFromBuffer(webp)).toBe('image/webp'); }); it('detects video/mp4 from ftyp brands', () => { const mp4 = Buffer.from([ 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x31, 0x00, 0x00, 0x00, 0x00, 0x69, 0x73, 0x6f, 0x6d, ]); expect(detectMimeTypeFromBuffer(mp4)).toBe('video/mp4'); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.ts` around lines 1 - 25, Add coverage in the detectMimeTypeFromBuffer test suite for JPEG, PNG, GIF, WEBP, and ftyp-based video/mp4 detection using minimal crafted buffers matching each format’s magic bytes. Keep the existing HEIC and unknown-byte tests unchanged, and assert the expected MIME type for each new case.src/lib/telegram/chat/resolveTelegramFileMimeType.ts (1)
1-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the
@/*alias for all new local TypeScript imports.
src/lib/telegram/chat/resolveTelegramFileMimeType.ts#L1-L2: replace both relative imports with their@/lib/telegram/chat/...aliases.src/lib/telegram/chat/__tests__/needsMimeSniff.test.ts#L2-L2: replace../needsMimeSniffwith@/lib/telegram/chat/needsMimeSniff.src/lib/telegram/chat/__tests__/resolveTelegramFileMimeType.test.ts#L4-L4: replace../resolveTelegramFileMimeTypewith@/lib/telegram/chat/resolveTelegramFileMimeType.As per path instructions, local TypeScript imports targeting
src/*must use the@/*alias.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/telegram/chat/resolveTelegramFileMimeType.ts` around lines 1 - 2, Replace the relative local TypeScript imports with the `@/`* alias: update both imports in src/lib/telegram/chat/resolveTelegramFileMimeType.ts lines 1-2, the needsMimeSniff import in src/lib/telegram/chat/__tests__/needsMimeSniff.test.ts line 2, and the resolveTelegramFileMimeType import in src/lib/telegram/chat/__tests__/resolveTelegramFileMimeType.test.ts line 4 to reference their `@/lib/telegram/chat/`... paths.Source: Path instructions
src/lib/telegram/chat/patchExifrHeicDetection.ts (2)
29-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
this.typereliance is fragile despite being correct today.
thisinside the reassignedcanHandleonly resolves correctly because exifr's engine invokes it asFileParser.canHandle(file, marker). CapturingheicParser.typevia closure instead ofthis.typewould remove the dependency on how exifr internally calls the method.♻️ Optional refactor
const original = heicParser.canHandle.bind(heicParser); + const { type } = heicParser; heicParser.canHandle = function (file, firstTwoBytes) { if (firstTwoBytes !== 0) return false; if (file.getString(4, 4) !== 'ftyp') return false; const ftypLength = file.getUint32(0); if (ftypLength < 16 || ftypLength > 256) return false; for (let offset = 16; offset + 4 <= ftypLength; offset += 4) { - if (file.getString(offset, 4) === this.type) return true; + if (file.getString(offset, 4) === type) return true; } return original(file, firstTwoBytes); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/telegram/chat/patchExifrHeicDetection.ts` around lines 29 - 41, Update the reassigned heicParser.canHandle function to capture heicParser.type in a closure before the function definition, then compare brand strings against that captured value instead of this.type. Preserve the existing parsing checks and fallback to original unchanged.
13-26: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winPatch silently no-ops if exifr's internal
fileParsersshape changes.
exifr.fileParsersandcanHandle's shape are undocumented internals (confirmed againstExifr.mjs, which callsFileParser.canHandle(file, marker)withmarker = file.getUint16(0)— consistent with what this patch assumes). BecausepatchExifrHeicDetectionjustreturns whenheicParseris missing/reshaped, a future exifr bump could silently drop this fix with no runtime signal — only a fixture-test failure would catch it, and only if that test is run.Consider logging a warning when the parser can't be found/patched, so a regression surfaces immediately rather than depending solely on CI catching it.
🔧 Suggested defensive logging
if (!heicParser) return; + if (!heicParser) { + console.warn('[patchExifrHeicDetection] exifr heic parser not found; HEIC multi-brand detection patch not applied.'); + return; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/telegram/chat/patchExifrHeicDetection.ts` around lines 13 - 26, Update patchExifrHeicDetection so it emits a warning when exifr.fileParsers does not provide the expected heicParser or compatible canHandle shape, rather than silently returning. Keep the existing successful patch behavior unchanged and use the module’s established logging mechanism to report that the HEIC detection patch could not be applied.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/telegram/chat/detectMimeTypeFromBuffer.ts`:
- Around line 1-2: Replace the relative internal imports with the `@/`* alias in
src/lib/telegram/chat/detectMimeTypeFromBuffer.ts lines 1-2, using
`@/lib/telegram/chat/bufferStartsWith` and `@/lib/telegram/chat/readFtypBrands`;
update src/lib/telegram/chat/__tests__/bufferStartsWith.test.ts line 2,
src/lib/telegram/chat/__tests__/readFtypBrands.test.ts line 4, and
src/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.ts line 4 to use
the corresponding `@/lib/telegram/chat` imports.
In `@src/lib/telegram/chat/readFtypBrands.ts`:
- Around line 9-13: Update the brand extraction logic in the visible
readFtypBrands flow to include the major brand from bytes 8–11 before collecting
compatible brands from offset 16. Preserve the existing compatible-brand
iteration and return the major brand first in the brands array so downstream
detectMimeTypeFromBuffer allowlist checks cover both brand types.
---
Nitpick comments:
In `@src/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.ts`:
- Around line 1-25: Add coverage in the detectMimeTypeFromBuffer test suite for
JPEG, PNG, GIF, WEBP, and ftyp-based video/mp4 detection using minimal crafted
buffers matching each format’s magic bytes. Keep the existing HEIC and
unknown-byte tests unchanged, and assert the expected MIME type for each new
case.
In `@src/lib/telegram/chat/bufferStartsWith.ts`:
- Around line 1-2: Optionally update bufferStartsWith to first verify that
buffer is at least as long as magic, returning false when it is shorter, then
preserve the existing byte-by-byte comparison for valid lengths.
In `@src/lib/telegram/chat/patchExifrHeicDetection.ts`:
- Around line 29-41: Update the reassigned heicParser.canHandle function to
capture heicParser.type in a closure before the function definition, then
compare brand strings against that captured value instead of this.type. Preserve
the existing parsing checks and fallback to original unchanged.
- Around line 13-26: Update patchExifrHeicDetection so it emits a warning when
exifr.fileParsers does not provide the expected heicParser or compatible
canHandle shape, rather than silently returning. Keep the existing successful
patch behavior unchanged and use the module’s established logging mechanism to
report that the HEIC detection patch could not be applied.
In `@src/lib/telegram/chat/resolveTelegramFileMimeType.ts`:
- Around line 1-2: Replace the relative local TypeScript imports with the `@/`*
alias: update both imports in
src/lib/telegram/chat/resolveTelegramFileMimeType.ts lines 1-2, the
needsMimeSniff import in src/lib/telegram/chat/__tests__/needsMimeSniff.test.ts
line 2, and the resolveTelegramFileMimeType import in
src/lib/telegram/chat/__tests__/resolveTelegramFileMimeType.test.ts line 4 to
reference their `@/lib/telegram/chat/`... paths.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f790aab2-b0e6-4ee5-bfcd-2be3ee29470c
📒 Files selected for processing (28)
src/lib/telegram/chat/__tests__/bufferStartsWith.test.tssrc/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.tssrc/lib/telegram/chat/__tests__/extractExifCaptureDate.heic.test.tssrc/lib/telegram/chat/__tests__/extractExifCaptureDate.test.tssrc/lib/telegram/chat/__tests__/fetchTelegramFile.test.tssrc/lib/telegram/chat/__tests__/fixtures/apple-styled-photo.heicsrc/lib/telegram/chat/__tests__/fixtures/shelf-christmas-decoration.heicsrc/lib/telegram/chat/__tests__/getMimeTypeFromFilePath.test.tssrc/lib/telegram/chat/__tests__/needsMimeSniff.test.tssrc/lib/telegram/chat/__tests__/parseExifOffsetMs.test.tssrc/lib/telegram/chat/__tests__/readFtypBrands.test.tssrc/lib/telegram/chat/__tests__/resolveExifOffsetMs.test.tssrc/lib/telegram/chat/__tests__/resolveTelegramFileMimeType.test.tssrc/lib/telegram/chat/__tests__/resolveTelegramMediaAttachment.test.tssrc/lib/telegram/chat/bufferStartsWith.tssrc/lib/telegram/chat/detectMimeTypeFromBuffer.tssrc/lib/telegram/chat/extractExifCaptureDate.tssrc/lib/telegram/chat/fetchTelegramFile.tssrc/lib/telegram/chat/getMimeTypeFromFilePath.tssrc/lib/telegram/chat/handlers/__tests__/onNewMention.test.tssrc/lib/telegram/chat/handlers/onNewMention.tssrc/lib/telegram/chat/needsMimeSniff.tssrc/lib/telegram/chat/parseExifOffsetMs.tssrc/lib/telegram/chat/patchExifrHeicDetection.tssrc/lib/telegram/chat/readFtypBrands.tssrc/lib/telegram/chat/resolveExifOffsetMs.tssrc/lib/telegram/chat/resolveTelegramFileMimeType.tssrc/lib/telegram/chat/resolveTelegramMediaAttachment.ts
👮 Files not reviewed due to content moderation or server errors (2)
- src/lib/telegram/chat/tests/fixtures/apple-styled-photo.heic
- src/lib/telegram/chat/tests/fixtures/shelf-christmas-decoration.heic
| import bufferStartsWith from './bufferStartsWith'; | ||
| import readFtypBrands from './readFtypBrands'; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use @/* path alias for internal imports per coding guidelines.
All internal imports use relative paths instead of the @/* alias. Per the project coding guidelines, imports pointing to ./src/* should use the @/* path alias.
src/lib/telegram/chat/detectMimeTypeFromBuffer.ts#L1-L2: replace./bufferStartsWithand./readFtypBrandswith@/lib/telegram/chat/bufferStartsWithand@/lib/telegram/chat/readFtypBrandssrc/lib/telegram/chat/__tests__/bufferStartsWith.test.ts#L2: replace../bufferStartsWithwith@/lib/telegram/chat/bufferStartsWithsrc/lib/telegram/chat/__tests__/readFtypBrands.test.ts#L4: replace../readFtypBrandswith@/lib/telegram/chat/readFtypBrandssrc/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.ts#L4: replace../detectMimeTypeFromBufferwith@/lib/telegram/chat/detectMimeTypeFromBuffer
As per coding guidelines: **/*.{ts,tsx,js,jsx}: Use path alias @/* for imports pointing to ./src/*.
📍 Affects 4 files
src/lib/telegram/chat/detectMimeTypeFromBuffer.ts#L1-L2(this comment)src/lib/telegram/chat/__tests__/bufferStartsWith.test.ts#L2-L2src/lib/telegram/chat/__tests__/readFtypBrands.test.ts#L4-L4src/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.ts#L4-L4
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/telegram/chat/detectMimeTypeFromBuffer.ts` around lines 1 - 2,
Replace the relative internal imports with the `@/`* alias in
src/lib/telegram/chat/detectMimeTypeFromBuffer.ts lines 1-2, using
`@/lib/telegram/chat/bufferStartsWith` and `@/lib/telegram/chat/readFtypBrands`;
update src/lib/telegram/chat/__tests__/bufferStartsWith.test.ts line 2,
src/lib/telegram/chat/__tests__/readFtypBrands.test.ts line 4, and
src/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.ts line 4 to use
the corresponding `@/lib/telegram/chat` imports.
Source: Coding guidelines
| const brands: string[] = []; | ||
| for (let offset = 16; offset + 4 <= ftypLength; offset += 4) { | ||
| brands.push(buffer.toString('ascii', offset, offset + 4)); | ||
| } | ||
| return brands; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Include the major brand in the returned brands array.
The loop starts at offset 16 (compatible brands) but skips the major brand at bytes 8–11. Per the ISO BMFF spec, the major brand is a valid brand that should be checked against allowlists. If a file's major brand is heic but it's not listed among compatible brands, detectMimeTypeFromBuffer will fail to classify it.
🐛 Proposed fix: prepend major brand to brands array
const brands: string[] = [];
+ // Include the major brand (bytes 8–11) per ISO BMFF spec
+ brands.push(buffer.toString('ascii', 8, 12));
for (let offset = 16; offset + 4 <= ftypLength; offset += 4) {
brands.push(buffer.toString('ascii', offset, offset + 4));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const brands: string[] = []; | |
| for (let offset = 16; offset + 4 <= ftypLength; offset += 4) { | |
| brands.push(buffer.toString('ascii', offset, offset + 4)); | |
| } | |
| return brands; | |
| const brands: string[] = []; | |
| // Include the major brand (bytes 8–11) per ISO BMFF spec | |
| brands.push(buffer.toString('ascii', 8, 12)); | |
| for (let offset = 16; offset + 4 <= ftypLength; offset += 4) { | |
| brands.push(buffer.toString('ascii', offset, offset + 4)); | |
| } | |
| return brands; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/telegram/chat/readFtypBrands.ts` around lines 9 - 13, Update the
brand extraction logic in the visible readFtypBrands flow to include the major
brand from bytes 8–11 before collecting compatible brands from offset 16.
Preserve the existing compatible-brand iteration and return the major brand
first in the brands array so downstream detectMimeTypeFromBuffer allowlist
checks cover both brand types.
Summary
saleStartfrom EXIF usingOffsetTimeOriginalorOffsetTimewithreviveValues: false(no timezone guessing).Test plan
pnpm exec vitest run src/lib/telegram/chat(245 tests passed)shelf-christmas-decoration.heic→2023-12-13T12:03:39Zapple-styled-photo.heic(111.HEIC) →2026-07-05T16:02:17Z111.HEICas a Telegram document with caption and verify moment timeline uses capture timeMade with Cursor
Summary by CodeRabbit