diff --git a/worker/src/db/queries.ts b/worker/src/db/queries.ts index ef3101f..a899336 100644 --- a/worker/src/db/queries.ts +++ b/worker/src/db/queries.ts @@ -1012,12 +1012,25 @@ export async function createAutoGroup(input: { // ── Provenance backfill ── +export interface PlaceholderCandidate { + id: string; + archiveType: string; + fileCount: number; + fileSize: bigint; + destMessageId: bigint | null; + destMessageIds: bigint[]; + destChannel: { telegramId: bigint } | null; +} + export async function findPlaceholderCandidate( destChannelId: string, fileName: string, fileSize: bigint, -): Promise<{ id: string; archiveType: string; fileCount: number } | null> { - return db.package.findFirst({ +): Promise { + // Package has no direct `destChannel` relation (only the scalar + // `destChannelId`), so resolve the destination TelegramChannel's + // telegramId with a follow-up lookup rather than a Prisma include. + const row = await db.package.findFirst({ where: { fileName, fileSize, @@ -1029,9 +1042,28 @@ export async function findPlaceholderCandidate( { sourceMessageId: 0n }, ], }, - select: { id: true, archiveType: true, fileCount: true }, + select: { + id: true, archiveType: true, fileCount: true, fileSize: true, + destMessageId: true, destMessageIds: true, destChannelId: true, + }, orderBy: { indexedAt: "asc" }, }); + if (!row) return null; + const destChannel = row.destChannelId + ? await db.telegramChannel.findUnique({ + where: { id: row.destChannelId }, + select: { telegramId: true }, + }) + : null; + return { + id: row.id, + archiveType: row.archiveType, + fileCount: row.fileCount, + fileSize: row.fileSize, + destMessageId: row.destMessageId, + destMessageIds: row.destMessageIds, + destChannel, + }; } export async function getPackageFileCrcs(packageId: string): Promise<(string | null)[]> { diff --git a/worker/src/provenance-backfill.ts b/worker/src/provenance-backfill.ts index da17dd6..b656a8e 100644 --- a/worker/src/provenance-backfill.ts +++ b/worker/src/provenance-backfill.ts @@ -1,7 +1,8 @@ import { childLogger } from "./util/logger.js"; import { downloadFileRange } from "./tdlib/range-download.js"; +import { invokeWithTimeout } from "./tdlib/download.js"; import { parseZipCentralDirectoryFromTail, MIN_ZIP_TAIL_BYTES } from "./archive/central-directory.js"; -import { fingerprintsMatch } from "./archive/fingerprint.js"; +import { fingerprintsMatch, crcFingerprint } from "./archive/fingerprint.js"; import { findPlaceholderCandidate, getPackageFileCrcs, @@ -49,6 +50,28 @@ async function readScannedZipListing( return null; } +async function readZipListingFromDestination( + client: Client, + destChatTelegramId: bigint, + destMessageId: bigint, + fileSize: bigint, +): Promise { + try { + // Resolve the destination message's document file id. + const msg = (await invokeWithTimeout(client, { + _: "getMessage", + chat_id: Number(destChatTelegramId), + message_id: Number(destMessageId), + })) as { content?: { document?: { document?: { id: number } } } }; + const fid = msg?.content?.document?.document?.id; + if (!fid) return null; + return await readScannedZipListing(client, String(fid), fileSize); + } catch (err) { + log.warn({ err, destMessageId: Number(destMessageId) }, "destination ZIP listing read failed"); + return null; + } +} + export async function tryProvenanceBackfill( args: BackfillArgs, ): Promise<{ backfilled: boolean; confidence?: "fingerprint" | "name-size" }> { @@ -62,9 +85,24 @@ export async function tryProvenanceBackfill( entries = await readScannedZipListing(args.client, args.scannedFileId, args.fileSize); if (entries) { const candidateCrcs = await getPackageFileCrcs(candidate.id); - const candidateEntries: FileEntry[] = candidateCrcs.map((crc) => ({ + let candidateEntries: FileEntry[] = candidateCrcs.map((crc) => ({ path: "", fileName: "", extension: null, compressedSize: 0n, uncompressedSize: 0n, crc32: crc, })); + const destMessageId = + candidate.destMessageIds.length > 0 + ? candidate.destMessageIds[candidate.destMessageIds.length - 1] + : candidate.destMessageId; + if (!crcFingerprint(candidateEntries).complete && destMessageId && candidate.destChannel) { + const destEntries = await readZipListingFromDestination( + args.client, + candidate.destChannel.telegramId, + destMessageId, + candidate.fileSize, + ); + if (destEntries) { + candidateEntries = destEntries; + } + } if (fingerprintsMatch(entries, candidateEntries)) { confidence = "fingerprint"; } else {