Position Telegram bulk-upload moments by EXIF capture date#400
Conversation
Telegram bulk uploads currently position moments on the Timeline by upload time instead of when the photo was actually taken. Extract the EXIF DateTimeOriginal from the image buffer and use it as saleStart, falling back to upload time when EXIF is absent (Telegram strips it from compressed "photo" sends). Also fixes document-mode uploads: images/videos sent as a Telegram document (which preserves EXIF, unlike compressed photo sends) were rejected outright, then would have been misrouted into the video/Mux upload path, and their file_id couldn't be resolved. Attachment type is now normalized by mimeType at the entry point so document-sent images/videos flow through the same path as their compressed counterparts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds EXIF-based capture date extraction (via the new ChangesEXIF capture date and media attachment resolution
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant TelegramMessage
participant createMomentsFromGroup
participant extractExifCaptureDate
participant buildCreateBatchInput
TelegramMessage->>createMomentsFromGroup: pending media assets
createMomentsFromGroup->>createMomentsFromGroup: prefetch file buffer per asset
createMomentsFromGroup->>extractExifCaptureDate: parse EXIF for image assets
extractExifCaptureDate-->>createMomentsFromGroup: captureDate or undefined
createMomentsFromGroup->>buildCreateBatchInput: uploaded items with captureDate
buildCreateBatchInput->>buildCreateBatchInput: saleStart = captureDate ?? uploadTime
buildCreateBatchInput-->>createMomentsFromGroup: batch input with per-token saleStart
sequenceDiagram
participant TelegramMessage
participant onNewMention
participant resolveMediaAttachmentType
participant processMediaThread
participant extractTelegramFileIds
TelegramMessage->>onNewMention: attachment (document/image/video)
onNewMention->>resolveMediaAttachmentType: resolve attachment type
resolveMediaAttachmentType-->>onNewMention: 'image' | 'video' | undefined
alt resolvedType present
onNewMention->>processMediaThread: attachment with resolved type
processMediaThread->>extractTelegramFileIds: message, attachment
extractTelegramFileIds-->>processMediaThread: fileId, thumbFileId
else no resolvedType
onNewMention->>TelegramMessage: post fallback instructional message
end
🚥 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__/buildCreateBatchInput.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.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__/extractTelegramFileIds.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
🤖 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/createMomentsFromGroup.ts`:
- Around line 38-56: The attachment creation in createMomentsFromGroup is
dropping the mimeType returned by fetchTelegramFile and relying only on
asset.mimeType, which can be undefined. Update the pending.map handler to keep
the fetched mimeType from fetchTelegramFile and use it as a fallback when
building the Attachment in processAttachmentUpload, preserving the existing
asset.mimeType when present. Reference the fetchTelegramFile call and the
Attachment object in createMomentsFromGroup so the content type is always
populated.
In `@src/lib/telegram/chat/handlers/onNewMention.ts`:
- Line 8: The import in onNewMention should use the `@/`* path alias instead of a
relative path. Update the resolveMediaAttachmentType import in onNewMention to
point to `@/lib/telegram/chat/resolveMediaAttachmentType` so it matches the
project import convention and avoids relative traversal.
🪄 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: 631cd1b6-2bdd-4794-9160-b71d1dd76fc9
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (13)
package.jsonsrc/lib/telegram/chat/__tests__/buildCreateBatchInput.test.tssrc/lib/telegram/chat/__tests__/extractExifCaptureDate.test.tssrc/lib/telegram/chat/__tests__/extractTelegramFileIds.test.tssrc/lib/telegram/chat/__tests__/resolveMediaAttachmentType.test.tssrc/lib/telegram/chat/buildCreateBatchInput.tssrc/lib/telegram/chat/createMomentsFromGroup.tssrc/lib/telegram/chat/extractExifCaptureDate.tssrc/lib/telegram/chat/extractTelegramFileIds.tssrc/lib/telegram/chat/handlers/__tests__/onNewMention.test.tssrc/lib/telegram/chat/handlers/onNewMention.tssrc/lib/telegram/chat/processMediaThread.tssrc/lib/telegram/chat/resolveMediaAttachmentType.ts
| pending.map(async (asset) => { | ||
| const { buffer } = await fetchTelegramFile(asset.fileId); | ||
| const attachment: Attachment = { | ||
| type: asset.attachmentType, | ||
| mimeType: asset.mimeType, | ||
| fetchData: () => | ||
| fetchTelegramFile(asset.fileId).then((r) => r.buffer), | ||
| fetchData: () => Promise.resolve(buffer), | ||
| }; | ||
| return processAttachmentUpload( | ||
| attachment, | ||
| asset.fileId, | ||
| asset.name, | ||
| asset.thumbFileId | ||
| ); | ||
| const [uploadResult, captureDate] = await Promise.all([ | ||
| processAttachmentUpload( | ||
| attachment, | ||
| asset.fileId, | ||
| asset.name, | ||
| asset.thumbFileId | ||
| ), | ||
| asset.attachmentType === 'image' | ||
| ? extractExifCaptureDate(buffer) | ||
| : Promise.resolve(undefined), | ||
| ]); | ||
| return { ...uploadResult, captureDate }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Use fetchTelegramFile's mimeType as a fallback for the attachment.
fetchTelegramFile returns { buffer, mimeType } but only buffer is destructured. The attachment uses asset.mimeType, which is optional on PendingMediaGroupAsset and could be undefined. The fetched mimeType (derived from the Telegram file path) is discarded, potentially leaving the attachment without a content type even though one was computed.
Proposed fix
- const { buffer } = await fetchTelegramFile(asset.fileId);
+ const { buffer, mimeType } = await fetchTelegramFile(asset.fileId);
const attachment: Attachment = {
type: asset.attachmentType,
- mimeType: asset.mimeType,
+ mimeType: asset.mimeType ?? mimeType,
fetchData: () => Promise.resolve(buffer),
};📝 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.
| pending.map(async (asset) => { | |
| const { buffer } = await fetchTelegramFile(asset.fileId); | |
| const attachment: Attachment = { | |
| type: asset.attachmentType, | |
| mimeType: asset.mimeType, | |
| fetchData: () => | |
| fetchTelegramFile(asset.fileId).then((r) => r.buffer), | |
| fetchData: () => Promise.resolve(buffer), | |
| }; | |
| return processAttachmentUpload( | |
| attachment, | |
| asset.fileId, | |
| asset.name, | |
| asset.thumbFileId | |
| ); | |
| const [uploadResult, captureDate] = await Promise.all([ | |
| processAttachmentUpload( | |
| attachment, | |
| asset.fileId, | |
| asset.name, | |
| asset.thumbFileId | |
| ), | |
| asset.attachmentType === 'image' | |
| ? extractExifCaptureDate(buffer) | |
| : Promise.resolve(undefined), | |
| ]); | |
| return { ...uploadResult, captureDate }; | |
| pending.map(async (asset) => { | |
| const { buffer, mimeType } = await fetchTelegramFile(asset.fileId); | |
| const attachment: Attachment = { | |
| type: asset.attachmentType, | |
| mimeType: asset.mimeType ?? mimeType, | |
| fetchData: () => Promise.resolve(buffer), | |
| }; | |
| const [uploadResult, captureDate] = await Promise.all([ | |
| processAttachmentUpload( | |
| attachment, | |
| asset.fileId, | |
| asset.name, | |
| asset.thumbFileId | |
| ), | |
| asset.attachmentType === 'image' | |
| ? extractExifCaptureDate(buffer) | |
| : Promise.resolve(undefined), | |
| ]); | |
| return { ...uploadResult, captureDate }; |
🤖 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/createMomentsFromGroup.ts` around lines 38 - 56, The
attachment creation in createMomentsFromGroup is dropping the mimeType returned
by fetchTelegramFile and relying only on asset.mimeType, which can be undefined.
Update the pending.map handler to keep the fetched mimeType from
fetchTelegramFile and use it as a fallback when building the Attachment in
processAttachmentUpload, preserving the existing asset.mimeType when present.
Reference the fetchTelegramFile call and the Attachment object in
createMomentsFromGroup so the content type is always populated.
| import parseTelegramChatId from '@/lib/telegram/parseTelegramChatId'; | ||
| import commandsHandler from '../commands/commandsHandler'; | ||
| import processMediaThread from '../processMediaThread'; | ||
| import resolveMediaAttachmentType from '../resolveMediaAttachmentType'; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use @/* path alias instead of relative import.
Same guideline violation as the test file — ../resolveMediaAttachmentType should be @/lib/telegram/chat/resolveMediaAttachmentType.
As per coding guidelines: **/*.{ts,tsx,js,jsx}: Use path alias @/* for imports pointing to ./src/*.
🤖 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/handlers/onNewMention.ts` at line 8, The import in
onNewMention should use the `@/`* path alias instead of a relative path. Update
the resolveMediaAttachmentType import in onNewMention to point to
`@/lib/telegram/chat/resolveMediaAttachmentType` so it matches the project import
convention and avoids relative traversal.
Source: Coding guidelines
Summary
saleStart = Date.now()), not when the photo was actually taken. Now extracts EXIFDateTimeOriginalfrom the image buffer and uses it assaleStart, falling back to upload time when EXIF is absent.file_ideven if that gate were removed. Attachment type is now normalized by mimeType so document-sent images/videos are treated the same as their compressed counterparts.Test plan
pnpm vitest run src/lib/telegram— 40 files / 228 tests passingpnpm lintclean on touched filesDateTimeOriginal)Summary by CodeRabbit
New Features
Bug Fixes