mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-09-21 05:21:43 +00:00
merge: close silent-drop gaps in attachment detection
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { detectArchive, isArchiveAttachment } from "./detect.js";
|
||||
|
||||
describe("detectArchive — 7z numbered multipart (pack.7z.001, pack.7z.002, ...)", () => {
|
||||
describe("detectArchive — numbered volumes (pack.EXT.001, pack.EXT.002, ...)", () => {
|
||||
it("recognizes a 7z multipart part as an archive attachment", () => {
|
||||
expect(isArchiveAttachment("Lost Adventures Vol2.7z.001")).toBe(true);
|
||||
});
|
||||
@@ -12,7 +12,7 @@ describe("detectArchive — 7z numbered multipart (pack.7z.001, pack.7z.002, ...
|
||||
baseName: "Lost Adventures Vol2.7z",
|
||||
partNumber: 1,
|
||||
format: "7Z",
|
||||
pattern: "SEVENZ_NUMBERED",
|
||||
pattern: "ARCHIVE_NUMBERED",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,4 +36,249 @@ describe("detectArchive — 7z numbered multipart (pack.7z.001, pack.7z.002, ...
|
||||
pattern: "SINGLE",
|
||||
});
|
||||
});
|
||||
|
||||
it("recognizes numbered ZIP volumes", () => {
|
||||
expect(detectArchive("Big Pack.zip.001")).toEqual({
|
||||
baseName: "Big Pack.zip",
|
||||
partNumber: 1,
|
||||
format: "ZIP",
|
||||
pattern: "ARCHIVE_NUMBERED",
|
||||
});
|
||||
});
|
||||
|
||||
it("recognizes numbered RAR volumes (previously dropped silently)", () => {
|
||||
expect(detectArchive("Big Pack.rar.001")).toEqual({
|
||||
baseName: "Big Pack.rar",
|
||||
partNumber: 1,
|
||||
format: "RAR",
|
||||
pattern: "ARCHIVE_NUMBERED",
|
||||
});
|
||||
});
|
||||
|
||||
it("derives the format from the archive extension, not a hardcoded value", () => {
|
||||
expect(detectArchive("A.zip.004")?.format).toBe("ZIP");
|
||||
expect(detectArchive("A.RAR.004")?.format).toBe("RAR");
|
||||
expect(detectArchive("A.7z.004")?.format).toBe("7Z");
|
||||
});
|
||||
|
||||
it("accepts hand-renamed two-digit volumes for every format", () => {
|
||||
expect(detectArchive("Pack.zip.01")).toEqual({
|
||||
baseName: "Pack.zip",
|
||||
partNumber: 1,
|
||||
format: "ZIP",
|
||||
pattern: "ARCHIVE_NUMBERED",
|
||||
});
|
||||
expect(detectArchive("Pack.rar.02")?.partNumber).toBe(2);
|
||||
expect(detectArchive("Pack.7z.03")?.partNumber).toBe(3);
|
||||
});
|
||||
|
||||
it("still accepts four-or-more-digit volumes", () => {
|
||||
expect(detectArchive("Pack.7z.0001")).toEqual({
|
||||
baseName: "Pack.7z",
|
||||
partNumber: 1,
|
||||
format: "7Z",
|
||||
pattern: "ARCHIVE_NUMBERED",
|
||||
});
|
||||
expect(detectArchive("Pack.zip.10001")?.partNumber).toBe(10001);
|
||||
});
|
||||
|
||||
it("does not match a single-digit suffix (too ambiguous with real extensions)", () => {
|
||||
expect(detectArchive("Pack.zip.1")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectArchive — pattern ordering safety", () => {
|
||||
it("does not let ZIP_LEGACY swallow a .7z.NNN name", () => {
|
||||
expect(detectArchive("Pack.7z.001")?.pattern).toBe("ARCHIVE_NUMBERED");
|
||||
expect(detectArchive("Pack.7z.001")?.format).toBe("7Z");
|
||||
});
|
||||
|
||||
it("does not let ARCHIVE_NUMBERED swallow legacy .zNN names", () => {
|
||||
expect(detectArchive("Pack.z01")).toEqual({
|
||||
baseName: "Pack",
|
||||
partNumber: 1,
|
||||
format: "ZIP",
|
||||
pattern: "ZIP_LEGACY",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not let ARCHIVE_NUMBERED swallow legacy .rNN names", () => {
|
||||
expect(detectArchive("Pack.r00")).toEqual({
|
||||
baseName: "Pack",
|
||||
partNumber: 0,
|
||||
format: "RAR",
|
||||
pattern: "RAR_LEGACY",
|
||||
});
|
||||
expect(detectArchive("Pack.r01")?.pattern).toBe("RAR_LEGACY");
|
||||
});
|
||||
|
||||
it("keeps .partN.rar on the RAR_PART pattern", () => {
|
||||
expect(detectArchive("Pack.part2.rar")).toEqual({
|
||||
baseName: "Pack",
|
||||
partNumber: 2,
|
||||
format: "RAR",
|
||||
pattern: "RAR_PART",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectArchive — filename normalization", () => {
|
||||
it("recognizes a name with a trailing space and reports the trimmed baseName", () => {
|
||||
expect(detectArchive("Pack.zip ")).toEqual({
|
||||
baseName: "Pack",
|
||||
partNumber: -1,
|
||||
format: "ZIP",
|
||||
pattern: "SINGLE",
|
||||
});
|
||||
});
|
||||
|
||||
it("recognizes a name with a leading space", () => {
|
||||
expect(detectArchive(" Pack.rar")).toEqual({
|
||||
baseName: "Pack",
|
||||
partNumber: -1,
|
||||
format: "RAR",
|
||||
pattern: "SINGLE",
|
||||
});
|
||||
});
|
||||
|
||||
it("recognizes a multipart name with surrounding whitespace", () => {
|
||||
expect(detectArchive("\tPack.rar.002 \n")).toEqual({
|
||||
baseName: "Pack.rar",
|
||||
partNumber: 2,
|
||||
format: "RAR",
|
||||
pattern: "ARCHIVE_NUMBERED",
|
||||
});
|
||||
});
|
||||
|
||||
it("recognizes a name with a trailing dot", () => {
|
||||
expect(detectArchive("Pack.zip.")).toEqual({
|
||||
baseName: "Pack",
|
||||
partNumber: -1,
|
||||
format: "ZIP",
|
||||
pattern: "SINGLE",
|
||||
});
|
||||
});
|
||||
|
||||
it("recognizes a name with a trailing dot followed by a space", () => {
|
||||
expect(detectArchive("Pack.part3.rar. ")?.pattern).toBe("RAR_PART");
|
||||
});
|
||||
|
||||
it("still returns null for a whitespace-only or empty name", () => {
|
||||
expect(detectArchive(" ")).toBeNull();
|
||||
expect(detectArchive("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectArchive — self-extracting RAR first volume (.partN.exe)", () => {
|
||||
it("recognizes Pack.part1.exe as the first volume of a RAR_PART set", () => {
|
||||
expect(detectArchive("Pack.part1.exe")).toEqual({
|
||||
baseName: "Pack",
|
||||
partNumber: 1,
|
||||
format: "RAR",
|
||||
pattern: "RAR_PART",
|
||||
});
|
||||
});
|
||||
|
||||
it("groups the SFX first volume with its .rar continuation volumes", () => {
|
||||
const sfx = detectArchive("Pack.part1.exe");
|
||||
const cont = detectArchive("Pack.part2.rar");
|
||||
expect(sfx?.baseName).toBe(cont?.baseName);
|
||||
expect(sfx?.format).toBe(cont?.format);
|
||||
});
|
||||
|
||||
it("does not open the door to arbitrary .exe attachments", () => {
|
||||
expect(detectArchive("Installer.exe")).toBeNull();
|
||||
expect(detectArchive("Pack.exe")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectArchive — standalone documents", () => {
|
||||
it("keeps recognizing the pre-existing document extensions", () => {
|
||||
expect(detectArchive("Model.stl")).toEqual({
|
||||
baseName: "Model",
|
||||
partNumber: -1,
|
||||
format: "DOCUMENT",
|
||||
pattern: "SINGLE",
|
||||
});
|
||||
expect(detectArchive("Sheet.pdf")?.format).toBe("DOCUMENT");
|
||||
});
|
||||
|
||||
it("recognizes slicer-project formats", () => {
|
||||
for (const name of [
|
||||
"Bust.lys",
|
||||
"Bust.lyt",
|
||||
"Bust.lymesh",
|
||||
"Bust.chitubox",
|
||||
"Bust.ctp",
|
||||
"Bust.ctb",
|
||||
"Bust.cbddlp",
|
||||
"Bust.photon",
|
||||
"Bust.pwmx",
|
||||
"Bust.pwmo",
|
||||
"Bust.pws",
|
||||
"Bust.sl1",
|
||||
"Bust.goo",
|
||||
"Bust.phz",
|
||||
"Bust.pm3",
|
||||
"Bust.form",
|
||||
]) {
|
||||
expect(detectArchive(name), name).toEqual({
|
||||
baseName: "Bust",
|
||||
partNumber: -1,
|
||||
format: "DOCUMENT",
|
||||
pattern: "SINGLE",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("recognizes 3D-model and CAD formats", () => {
|
||||
for (const name of [
|
||||
"Bust.fbx",
|
||||
"Bust.ply",
|
||||
"Bust.glb",
|
||||
"Bust.gltf",
|
||||
"Bust.3ds",
|
||||
"Bust.max",
|
||||
"Bust.c4d",
|
||||
"Bust.ztl",
|
||||
"Bust.zpr",
|
||||
"Bust.mtl",
|
||||
"Bust.f3d",
|
||||
"Bust.scad",
|
||||
"Bust.igs",
|
||||
"Bust.iges",
|
||||
"Bust.sldprt",
|
||||
"Bust.skp",
|
||||
"Bust.wrl",
|
||||
]) {
|
||||
expect(detectArchive(name), name).toEqual({
|
||||
baseName: "Bust",
|
||||
partNumber: -1,
|
||||
format: "DOCUMENT",
|
||||
pattern: "SINGLE",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("recognizes the .blend1 autosave sibling of .blend", () => {
|
||||
expect(detectArchive("Scene.blend")?.format).toBe("DOCUMENT");
|
||||
expect(detectArchive("Scene.blend1")).toEqual({
|
||||
baseName: "Scene",
|
||||
partNumber: -1,
|
||||
format: "DOCUMENT",
|
||||
pattern: "SINGLE",
|
||||
});
|
||||
});
|
||||
|
||||
it("does NOT recognize image attachments (they must not become packages)", () => {
|
||||
for (const name of ["Preview.jpg", "Preview.jpeg", "Preview.png", "Preview.webp", "Preview.gif"]) {
|
||||
expect(detectArchive(name), name).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("returns null for unrelated files", () => {
|
||||
expect(detectArchive("notes.txt")).toBeNull();
|
||||
expect(detectArchive("song.mp3")).toBeNull();
|
||||
expect(detectArchive("noextension")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,25 +4,29 @@ export interface MultipartInfo {
|
||||
baseName: string;
|
||||
partNumber: number;
|
||||
format: ArchiveFormat;
|
||||
pattern: "ZIP_NUMBERED" | "ZIP_LEGACY" | "RAR_PART" | "RAR_LEGACY" | "SEVENZ_NUMBERED" | "SINGLE";
|
||||
pattern: "ARCHIVE_NUMBERED" | "ZIP_LEGACY" | "RAR_PART" | "RAR_LEGACY" | "SINGLE";
|
||||
}
|
||||
|
||||
const patterns: {
|
||||
regex: RegExp;
|
||||
format: ArchiveFormat;
|
||||
/** A fixed format, or one derived from the match for patterns spanning several formats. */
|
||||
format: ArchiveFormat | ((match: RegExpMatchArray) => ArchiveFormat);
|
||||
pattern: MultipartInfo["pattern"];
|
||||
getBaseName: (match: RegExpMatchArray) => string;
|
||||
getPartNumber: (match: RegExpMatchArray) => number;
|
||||
}[] = [
|
||||
// pack.zip.001, pack.zip.002
|
||||
// pack.zip.001, pack.rar.001, pack.7z.001 (numbered volume split — one pattern for
|
||||
// every format, so a new format can never be silently dropped for lack of its own entry).
|
||||
// {2,} digits also picks up hand-renamed sets like pack.rar.01.
|
||||
{
|
||||
regex: /^(.+\.zip)\.(\d{3,})$/i,
|
||||
format: "ZIP",
|
||||
pattern: "ZIP_NUMBERED",
|
||||
getBaseName: (m) => m[1],
|
||||
getPartNumber: (m) => parseInt(m[2], 10),
|
||||
regex: /^(.+\.(zip|7z|rar))\.(\d{2,})$/i,
|
||||
// The regex only ever captures zip/7z/rar, so uppercasing yields a valid ArchiveFormat.
|
||||
format: (m) => m[2].toUpperCase() as ArchiveFormat,
|
||||
pattern: "ARCHIVE_NUMBERED",
|
||||
getBaseName: (m) => m[1], // includes the archive extension
|
||||
getPartNumber: (m) => parseInt(m[3], 10),
|
||||
},
|
||||
// pack.z01, pack.z02 (legacy split — final part is pack.zip)
|
||||
// pack.z01, pack.z02 (legacy split — pack.zip is the FINAL disk of the set)
|
||||
{
|
||||
regex: /^(.+)\.z(\d{2,})$/i,
|
||||
format: "ZIP",
|
||||
@@ -30,15 +34,16 @@ const patterns: {
|
||||
getBaseName: (m) => m[1],
|
||||
getPartNumber: (m) => parseInt(m[2], 10),
|
||||
},
|
||||
// pack.part1.rar, pack.part2.rar
|
||||
// pack.part1.rar, pack.part2.rar — .exe covers a self-extracting first volume
|
||||
// (pack.part1.exe + pack.part2.rar + ...), which is still a RAR volume set.
|
||||
{
|
||||
regex: /^(.+)\.part(\d+)\.rar$/i,
|
||||
regex: /^(.+)\.part(\d+)\.(rar|exe)$/i,
|
||||
format: "RAR",
|
||||
pattern: "RAR_PART",
|
||||
getBaseName: (m) => m[1],
|
||||
getPartNumber: (m) => parseInt(m[2], 10),
|
||||
},
|
||||
// pack.r00, pack.r01 (legacy split — final part is pack.rar)
|
||||
// pack.r00, pack.r01 (legacy split — pack.rar is the FIRST volume, .r00 onwards follow it)
|
||||
{
|
||||
regex: /^(.+)\.r(\d{2,})$/i,
|
||||
format: "RAR",
|
||||
@@ -46,50 +51,53 @@ const patterns: {
|
||||
getBaseName: (m) => m[1],
|
||||
getPartNumber: (m) => parseInt(m[2], 10),
|
||||
},
|
||||
// pack.7z.001, pack.7z.002 (native 7z volume split)
|
||||
{
|
||||
regex: /^(.+\.7z)\.(\d{3,})$/i,
|
||||
format: "7Z",
|
||||
pattern: "SEVENZ_NUMBERED",
|
||||
getBaseName: (m) => m[1],
|
||||
getPartNumber: (m) => parseInt(m[2], 10),
|
||||
},
|
||||
];
|
||||
|
||||
/** Extensions we recognize as fetchable documents (archives + standalone files) */
|
||||
const DOCUMENT_EXTENSIONS = /\.(pdf|stl|obj|3mf|step|stp|blend|gcode|svg|dxf|ai|eps|psd)$/i;
|
||||
/** Extensions we recognize as fetchable documents (archives + standalone files).
|
||||
* Deliberately excludes image formats — previews posted as uncompressed documents
|
||||
* must go through the photo-matching path, not become packages of their own. */
|
||||
const DOCUMENT_EXTENSIONS =
|
||||
/\.(pdf|stl|obj|3mf|step|stp|blend1|blend|gcode|svg|dxf|ai|eps|psd|lys|lyt|lymesh|chitubox|ctp|ctb|cbddlp|photon|pwmx|pwmo|pws|sl1|goo|phz|pm3|fbx|ply|glb|gltf|3ds|max|c4d|ztl|zpr|mtl|f3d|scad|igs|iges|sldprt|form|skp|wrl)$/i;
|
||||
|
||||
/**
|
||||
* Detect if a filename is an archive and extract multipart info.
|
||||
*/
|
||||
export function detectArchive(fileName: string): MultipartInfo | null {
|
||||
// TDLib hands us `document.file_name` verbatim and every pattern below is `$`-anchored,
|
||||
// so "Pack.zip " or "Pack.zip." would otherwise be dropped without a trace. Trailing dots
|
||||
// are stripped too: no filesystem or archiver can produce a meaningful one (Windows
|
||||
// silently drops them), so a trailing dot is always cosmetic damage from a re-upload,
|
||||
// never part of the real name. The normalized value is used for matching AND baseName.
|
||||
const name = fileName.trim().replace(/[.\s]+$/, "");
|
||||
if (!name) return null;
|
||||
|
||||
// Check multipart patterns first
|
||||
for (const p of patterns) {
|
||||
const match = fileName.match(p.regex);
|
||||
const match = name.match(p.regex);
|
||||
if (match) {
|
||||
return {
|
||||
baseName: p.getBaseName(match),
|
||||
partNumber: p.getPartNumber(match),
|
||||
format: p.format,
|
||||
format: typeof p.format === "function" ? p.format(match) : p.format,
|
||||
pattern: p.pattern,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Single .zip file — could be a standalone or the final part of a ZIP_LEGACY set
|
||||
if (/\.zip$/i.test(fileName)) {
|
||||
if (/\.zip$/i.test(name)) {
|
||||
return {
|
||||
baseName: fileName.replace(/\.zip$/i, ""),
|
||||
baseName: name.replace(/\.zip$/i, ""),
|
||||
partNumber: -1, // -1 signals "could be single or final legacy part"
|
||||
format: "ZIP",
|
||||
pattern: "SINGLE",
|
||||
};
|
||||
}
|
||||
|
||||
// Single .rar file — could be standalone or final part of RAR_LEGACY set
|
||||
if (/\.rar$/i.test(fileName)) {
|
||||
// Single .rar file — could be standalone or the FIRST part of a RAR_LEGACY set
|
||||
if (/\.rar$/i.test(name)) {
|
||||
return {
|
||||
baseName: fileName.replace(/\.rar$/i, ""),
|
||||
baseName: name.replace(/\.rar$/i, ""),
|
||||
partNumber: -1,
|
||||
format: "RAR",
|
||||
pattern: "SINGLE",
|
||||
@@ -97,20 +105,19 @@ export function detectArchive(fileName: string): MultipartInfo | null {
|
||||
}
|
||||
|
||||
// Single .7z file
|
||||
if (/\.7z$/i.test(fileName)) {
|
||||
if (/\.7z$/i.test(name)) {
|
||||
return {
|
||||
baseName: fileName.replace(/\.7z$/i, ""),
|
||||
baseName: name.replace(/\.7z$/i, ""),
|
||||
partNumber: -1,
|
||||
format: "7Z",
|
||||
pattern: "SINGLE",
|
||||
};
|
||||
}
|
||||
|
||||
// Standalone documents (PDFs, STLs, 3D files, etc.)
|
||||
if (DOCUMENT_EXTENSIONS.test(fileName)) {
|
||||
const ext = fileName.match(DOCUMENT_EXTENSIONS)![0];
|
||||
// Standalone documents (PDFs, STLs, 3D files, slicer projects, etc.)
|
||||
if (DOCUMENT_EXTENSIONS.test(name)) {
|
||||
return {
|
||||
baseName: fileName.replace(DOCUMENT_EXTENSIONS, ""),
|
||||
baseName: name.replace(DOCUMENT_EXTENSIONS, ""),
|
||||
partNumber: -1,
|
||||
format: "DOCUMENT",
|
||||
pattern: "SINGLE",
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { groupArchiveSets, type TelegramMessage } from "./multipart.js";
|
||||
|
||||
let nextId = 1000n;
|
||||
|
||||
function msg(fileName: string): TelegramMessage {
|
||||
const id = nextId++;
|
||||
return {
|
||||
id,
|
||||
fileName,
|
||||
fileId: `file-${id}`,
|
||||
fileSize: 1024n,
|
||||
date: new Date("2026-01-01T00:00:00Z"),
|
||||
};
|
||||
}
|
||||
|
||||
function names(files: string[]): string[] {
|
||||
const sets = groupArchiveSets(files.map(msg));
|
||||
expect(sets).toHaveLength(1);
|
||||
return sets[0].parts.map((p) => p.fileName);
|
||||
}
|
||||
|
||||
describe("groupArchiveSets — legacy split part ordering", () => {
|
||||
it("puts the bare .rar FIRST in a RAR_LEGACY set (it is volume 1)", () => {
|
||||
expect(names(["Pack.r01", "Pack.rar", "Pack.r00"])).toEqual([
|
||||
"Pack.rar",
|
||||
"Pack.r00",
|
||||
"Pack.r01",
|
||||
]);
|
||||
});
|
||||
|
||||
it("puts the bare .zip LAST in a ZIP_LEGACY set (it is the final disk)", () => {
|
||||
expect(names(["Pack.z02", "Pack.zip", "Pack.z01"])).toEqual([
|
||||
"Pack.z01",
|
||||
"Pack.z02",
|
||||
"Pack.zip",
|
||||
]);
|
||||
});
|
||||
|
||||
it("marks both legacy sets as multipart with the right format", () => {
|
||||
const rar = groupArchiveSets([msg("Pack.rar"), msg("Pack.r00")])[0];
|
||||
expect(rar.isMultipart).toBe(true);
|
||||
expect(rar.type).toBe("RAR");
|
||||
|
||||
const zip = groupArchiveSets([msg("Pack.zip"), msg("Pack.z01")])[0];
|
||||
expect(zip.isMultipart).toBe(true);
|
||||
expect(zip.type).toBe("ZIP");
|
||||
});
|
||||
|
||||
it("orders numbered volume sets by part number", () => {
|
||||
expect(names(["Pack.rar.003", "Pack.rar.001", "Pack.rar.002"])).toEqual([
|
||||
"Pack.rar.001",
|
||||
"Pack.rar.002",
|
||||
"Pack.rar.003",
|
||||
]);
|
||||
});
|
||||
|
||||
it("orders .partN sets by part number with an SFX first volume", () => {
|
||||
expect(names(["Pack.part3.rar", "Pack.part1.exe", "Pack.part2.rar"])).toEqual([
|
||||
"Pack.part1.exe",
|
||||
"Pack.part2.rar",
|
||||
"Pack.part3.rar",
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats unrelated singles as their own non-multipart sets", () => {
|
||||
const sets = groupArchiveSets([msg("A.zip"), msg("B.rar")]);
|
||||
expect(sets).toHaveLength(2);
|
||||
expect(sets.every((s) => !s.isMultipart)).toBe(true);
|
||||
expect(sets.every((s) => s.parts.length === 1)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -78,10 +78,17 @@ export function groupArchiveSets(messages: TelegramMessage[]): ArchiveSet[] {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by part number (singles get a very high number so they come last — they're the final part)
|
||||
// Sort by part number. A bare single (partNumber -1) sits at a different end of the
|
||||
// set depending on the legacy scheme: in a .zip/.z01/.z02 set the bare pack.zip is the
|
||||
// FINAL disk, but in a .rar/.r00/.r01 set the bare pack.rar is volume 1 and .r00
|
||||
// onwards follow it. Getting this backwards makes parts[0] a headerless continuation
|
||||
// volume, which breaks listing and mislabels the package.
|
||||
const singleRank = multipartEntries.some((e) => e.info.pattern === "RAR_LEGACY")
|
||||
? -1 // before .r00
|
||||
: 999999;
|
||||
allEntries.sort((a, b) => {
|
||||
const aNum = a.info.partNumber === -1 ? 999999 : a.info.partNumber;
|
||||
const bNum = b.info.partNumber === -1 ? 999999 : b.info.partNumber;
|
||||
const aNum = a.info.partNumber === -1 ? singleRank : a.info.partNumber;
|
||||
const bNum = b.info.partNumber === -1 ? singleRank : b.info.partNumber;
|
||||
return aNum - bNum;
|
||||
});
|
||||
|
||||
|
||||
@@ -333,6 +333,14 @@ async function scanDestinationChannel(
|
||||
fileSize: BigInt(doc.document.size),
|
||||
date: new Date(msg.date * 1000),
|
||||
});
|
||||
} else if (doc?.file_name) {
|
||||
// Not matched by any pattern in archive/detect.ts, so it is dropped without a
|
||||
// packages/skipped_packages row. Grep "unrecognized attachment" to find naming
|
||||
// schemes we do not handle yet.
|
||||
log.debug(
|
||||
{ chatId: chatId.toString(), messageId: msg.id, fileName: doc.file_name },
|
||||
"Skipping unrecognized attachment (no archive/document pattern matched)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -267,6 +267,15 @@ export async function getChannelMessages(
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (doc?.file_name) {
|
||||
// Not matched by any pattern in archive/detect.ts, so it is dropped without a
|
||||
// packages/skipped_packages row. Grep "unrecognized attachment" to find naming
|
||||
// schemes we do not handle yet.
|
||||
log.debug(
|
||||
{ chatId: chatId.toString(), messageId: msg.id, fileName: doc.file_name },
|
||||
"Skipping unrecognized attachment (no archive/document pattern matched)"
|
||||
);
|
||||
}
|
||||
|
||||
// Check for photo messages (potential previews)
|
||||
const photo = msg.content?.photo;
|
||||
|
||||
@@ -280,6 +280,15 @@ export async function getTopicMessages(
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (doc?.file_name) {
|
||||
// Not matched by any pattern in archive/detect.ts, so it is dropped without a
|
||||
// packages/skipped_packages row. Grep "unrecognized attachment" to find naming
|
||||
// schemes we do not handle yet.
|
||||
log.debug(
|
||||
{ chatId: chatId.toString(), topicId: topicId.toString(), messageId: msg.id, fileName: doc.file_name },
|
||||
"Skipping unrecognized attachment (no archive/document pattern matched)"
|
||||
);
|
||||
}
|
||||
|
||||
// Check for photo messages (potential previews)
|
||||
const photo = msg.content?.photo;
|
||||
|
||||
Reference in New Issue
Block a user