Skip to content

Position Telegram bulk-upload moments by EXIF capture date#400

Merged
techeng322 merged 1 commit into
mainfrom
techengme/myc-5007-telegram-bulk-uploads
Jul 9, 2026
Merged

Position Telegram bulk-upload moments by EXIF capture date#400
techeng322 merged 1 commit into
mainfrom
techengme/myc-5007-telegram-bulk-uploads

Conversation

@techeng322

@techeng322 techeng322 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Telegram bulk uploads positioned moments on the Timeline by upload time (saleStart = Date.now()), not when the photo was actually taken. Now extracts EXIF DateTimeOriginal from the image buffer and uses it as saleStart, falling back to upload time when EXIF is absent.
  • Telegram strips EXIF from compressed "photo" sends but preserves it when sent as a "document". Sending as document was previously rejected outright at the entry point, and would have been misrouted into the video/Mux upload path with an unresolvable file_id even 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 passing
  • pnpm lint clean on touched files
  • Verified EXIF presence/absence against real sample images (compressed Telegram photo exports had no EXIF; a document-sent photo retained DateTimeOriginal)
  • Manual end-to-end test against the live bot (send an image as document vs. photo, confirm Timeline position)

Summary by CodeRabbit

  • New Features

    • Better handling for Telegram media uploads, including image and video files sent as documents.
    • Added support for reading photo capture dates when available, helping set more accurate sale timing.
  • Bug Fixes

    • Media batches now use each item’s own capture time when available, instead of applying one time to the whole batch.
    • Improved fallback behavior when capture data isn’t available, preserving upload-time defaults.

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>
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
in-process-api Ready Ready Preview Jul 9, 2026 2:31pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds EXIF-based capture date extraction (via the new exifr dependency) used to set per-token saleStart in batch creation, refactors Telegram file ID extraction to derive fileId from attachment metadata instead of raw message fields, and adds a resolveMediaAttachmentType utility to route document attachments to image/video processing based on MIME type in the mention handler.

Changes

EXIF capture date and media attachment resolution

Layer / File(s) Summary
EXIF capture date extraction utility
package.json, src/lib/telegram/chat/extractExifCaptureDate.ts, src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts
Adds exifr dependency and a new module parsing DateTimeOriginal into a UTC epoch-seconds timestamp, returning undefined on missing/invalid data or errors.
Per-token saleStart from captureDate
src/lib/telegram/chat/buildCreateBatchInput.ts, src/lib/telegram/chat/__tests__/buildCreateBatchInput.test.ts
UploadedMediaMeta gains optional captureDate; saleStart is now computed per token as captureDate ?? uploadTime instead of one shared batch timestamp.
Eager buffer fetch and EXIF wiring
src/lib/telegram/chat/createMomentsFromGroup.ts
Prefetches Telegram file buffers eagerly, runs EXIF capture-date extraction alongside upload for images, and passes captureDate into buildCreateBatchInput.
File ID extraction from attachment metadata
src/lib/telegram/chat/extractTelegramFileIds.ts, src/lib/telegram/chat/processMediaThread.ts, src/lib/telegram/chat/__tests__/extractTelegramFileIds.test.ts
extractTelegramFileIds now takes an attachment parameter and reads fileId from attachment.fetchMetadata, narrowing message.raw parsing to video thumbnails only; processMediaThread passes the attachment through.
Document media type resolution
src/lib/telegram/chat/resolveMediaAttachmentType.ts, src/lib/telegram/chat/handlers/onNewMention.ts, src/lib/telegram/chat/__tests__/resolveMediaAttachmentType.test.ts, src/lib/telegram/chat/handlers/__tests__/onNewMention.test.ts
New resolveMediaAttachmentType infers 'image'/'video' from document MIME types; onNewMention uses it to route document attachments to processMediaThread or fall back to an instructional message.

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
Loading
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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: using EXIF capture date to position Telegram bulk-upload moments.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch techengme/myc-5007-telegram-bulk-uploads

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/lib/telegram/chat/__tests__/buildCreateBatchInput.test.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/

... [truncated 446 characters] ...

c/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/

... [truncated 446 characters] ...

c/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

src/lib/telegram/chat/__tests__/extractTelegramFileIds.test.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/

... [truncated 446 characters] ...

c/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

  • 9 others

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between efbeb32 and 12154f3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • package.json
  • src/lib/telegram/chat/__tests__/buildCreateBatchInput.test.ts
  • src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts
  • src/lib/telegram/chat/__tests__/extractTelegramFileIds.test.ts
  • src/lib/telegram/chat/__tests__/resolveMediaAttachmentType.test.ts
  • src/lib/telegram/chat/buildCreateBatchInput.ts
  • src/lib/telegram/chat/createMomentsFromGroup.ts
  • src/lib/telegram/chat/extractExifCaptureDate.ts
  • src/lib/telegram/chat/extractTelegramFileIds.ts
  • src/lib/telegram/chat/handlers/__tests__/onNewMention.test.ts
  • src/lib/telegram/chat/handlers/onNewMention.ts
  • src/lib/telegram/chat/processMediaThread.ts
  • src/lib/telegram/chat/resolveMediaAttachmentType.ts

Comment on lines +38 to +56
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 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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

@techeng322 techeng322 merged commit 7b00e69 into main Jul 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant