// @effect-diagnostics nodeBuiltinImport:offimport * as NodeCrypto from "node:crypto";import * as NodeFS from "node:fs"; import type { ChatAttachment } from "@t3tools/contracts"; import { normalizeAttachmentRelativePath, resolveAttachmentRelativePath,} from "./attachmentPaths.ts";import { inferImageExtension, SAFE_IMAGE_FILE_EXTENSIONS } from "./imageMime.ts"; const ATTACHMENT_FILENAME_EXTENSIONS = [...SAFE_IMAGE_FILE_EXTENSIONS, ".bin"];const ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS = 80;const ATTACHMENT_ID_THREAD_SEGMENT_PATTERN = "[a-z0-9_]+(?:-[a-z0-9_]+)*";const ATTACHMENT_ID_UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";const ATTACHMENT_ID_PATTERN = new RegExp( `^(${ATTACHMENT_ID_THREAD_SEGMENT_PATTERN})-(${ATTACHMENT_ID_UUID_PATTERN})$`, "i",); export function toSafeThreadAttachmentSegment(threadId: string): string | null { const segment = threadId .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/gi, "-") .replace(/-+/g, "-") .replace(/^[-_]+|[-_]+$/g, "") .slice(0, ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS) .replace(/[-_]+$/g, ""); if (segment.length === 0) { return null; } return segment;} export function createAttachmentId(threadId: string): string | null { const threadSegment = toSafeThreadAttachmentSegment(threadId); if (!threadSegment) { return null; } return `${threadSegment}-${NodeCrypto.randomUUID()}`;} export function parseThreadSegmentFromAttachmentId(attachmentId: string): string | null { const normalizedId = normalizeAttachmentRelativePath(attachmentId); if (!normalizedId || normalizedId.includes("/") || normalizedId.includes(".")) { return null; } const match = normalizedId.match(ATTACHMENT_ID_PATTERN); if (!match) { return null; } return match[1]?.toLowerCase() ?? null;} export function attachmentRelativePath(attachment: ChatAttachment): string { switch (attachment.type) { case "image": { const extension = inferImageExtension({ mimeType: attachment.mimeType, fileName: attachment.name, }); return `${attachment.id}${extension}`; } }} export function resolveAttachmentPath(input: { readonly attachmentsDir: string; readonly attachment: ChatAttachment;}): string | null { return resolveAttachmentRelativePath({ attachmentsDir: input.attachmentsDir, relativePath: attachmentRelativePath(input.attachment), });} export function resolveAttachmentPathById(input: { readonly attachmentsDir: string; readonly attachmentId: string;}): string | null { const normalizedId = normalizeAttachmentRelativePath(input.attachmentId); if (!normalizedId || normalizedId.includes("/") || normalizedId.includes(".")) { return null; } for (const extension of ATTACHMENT_FILENAME_EXTENSIONS) { const maybePath = resolveAttachmentRelativePath({ attachmentsDir: input.attachmentsDir, relativePath: `${normalizedId}${extension}`, }); if (maybePath && NodeFS.existsSync(maybePath)) { return maybePath; } } return null;} export function parseAttachmentIdFromRelativePath(relativePath: string): string | null { const normalized = normalizeAttachmentRelativePath(relativePath); if (!normalized || normalized.includes("/")) { return null; } const extensionIndex = normalized.lastIndexOf("."); if (extensionIndex <= 0) { return null; } const id = normalized.slice(0, extensionIndex); return id.length > 0 && !id.includes(".") ? id : null;}
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeCrypto from "node:crypto";
import * as NodeFS from "node:fs";
import type { ChatAttachment } from "@t3tools/contracts";
import {
normalizeAttachmentRelativePath,
resolveAttachmentRelativePath,
} from "./attachmentPaths.ts";
import { inferImageExtension, SAFE_IMAGE_FILE_EXTENSIONS } from "./imageMime.ts";
const ATTACHMENT_FILENAME_EXTENSIONS = [...SAFE_IMAGE_FILE_EXTENSIONS, ".bin"];
const ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS = 80;
const ATTACHMENT_ID_THREAD_SEGMENT_PATTERN = "[a-z0-9_]+(?:-[a-z0-9_]+)*";
const ATTACHMENT_ID_UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
const ATTACHMENT_ID_PATTERN = new RegExp(
`^(${ATTACHMENT_ID_THREAD_SEGMENT_PATTERN})-(${ATTACHMENT_ID_UUID_PATTERN})$`,
"i",
);
export function toSafeThreadAttachmentSegment(threadId: string): string | null {
const segment = threadId
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]+/gi, "-")
.replace(/-+/g, "-")
.replace(/^[-_]+|[-_]+$/g, "")
.slice(0, ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS)
.replace(/[-_]+$/g, "");
if (segment.length === 0) {
return null;
}
return segment;
}
export function createAttachmentId(threadId: string): string | null {
const threadSegment = toSafeThreadAttachmentSegment(threadId);
if (!threadSegment) {
return null;
}
return `${threadSegment}-${NodeCrypto.randomUUID()}`;
}
export function parseThreadSegmentFromAttachmentId(attachmentId: string): string | null {
const normalizedId = normalizeAttachmentRelativePath(attachmentId);
if (!normalizedId || normalizedId.includes("/") || normalizedId.includes(".")) {
return null;
}
const match = normalizedId.match(ATTACHMENT_ID_PATTERN);
if (!match) {
return null;
}
return match[1]?.toLowerCase() ?? null;
}
export function attachmentRelativePath(attachment: ChatAttachment): string {
switch (attachment.type) {
case "image": {
const extension = inferImageExtension({
mimeType: attachment.mimeType,
fileName: attachment.name,
});
return `${attachment.id}${extension}`;
}
}
}
export function resolveAttachmentPath(input: {
readonly attachmentsDir: string;
readonly attachment: ChatAttachment;
}): string | null {
return resolveAttachmentRelativePath({
attachmentsDir: input.attachmentsDir,
relativePath: attachmentRelativePath(input.attachment),
});
}
export function resolveAttachmentPathById(input: {
readonly attachmentsDir: string;
readonly attachmentId: string;
}): string | null {
const normalizedId = normalizeAttachmentRelativePath(input.attachmentId);
if (!normalizedId || normalizedId.includes("/") || normalizedId.includes(".")) {
return null;
}
for (const extension of ATTACHMENT_FILENAME_EXTENSIONS) {
const maybePath = resolveAttachmentRelativePath({
attachmentsDir: input.attachmentsDir,
relativePath: `${normalizedId}${extension}`,
});
if (maybePath && NodeFS.existsSync(maybePath)) {
return maybePath;
}
}
return null;
}
export function parseAttachmentIdFromRelativePath(relativePath: string): string | null {
const normalized = normalizeAttachmentRelativePath(relativePath);
if (!normalized || normalized.includes("/")) {
return null;
}
const extensionIndex = normalized.lastIndexOf(".");
if (extensionIndex <= 0) {
return null;
}
const id = normalized.slice(0, extensionIndex);
return id.length > 0 && !id.includes(".") ? id : null;
}