Semua retry sekarang instant (retries=0, minTimeout=0, maxTimeout=0): - retry.ts (kedua) — default minTimeout/maxTimeout 0, factor 1 - config.ts (kedua) — DECODER_COOLDOWN_MS=0, AI_ANALYSIS_ERROR_COOLDOWN_MS=0 - llmModerationClient.ts (kedua) — retries 0 tanpa delay - aiAnalyzer.ts (kedua) — retries 0, AI_ANALYSIS_ERROR_COOLDOWN jadi 0 - indonesianTextNormalizer.ts (kedua) — semua rate limit cooldown 0 - recorder.ts (kedua), teleUpload (3 files), uploader (2 files) — retry instant - attachmentUploader (kedua) — retries 0 - syncRoutes.ts — BACKLOG_SYNC_COOLDOWN 0 20 files changed. Semua cooldown bottleneck dihapus. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
134 lines
3.5 KiB
TypeScript
134 lines
3.5 KiB
TypeScript
import { config } from "../../shared/config/config.js";
|
|
import { createChildLogger } from "../../shared/logger/logger.js";
|
|
import { uploadToTele } from "./teleUpload.js";
|
|
import {
|
|
updateAttachmentAsFailedUpload,
|
|
updateAttachmentAsUploaded,
|
|
updateAttachmentDiscordUrl,
|
|
} from "../message-capture/messageStore.js";
|
|
|
|
const logger = createChildLogger("attachment-uploader");
|
|
|
|
class AttachmentDownloadError extends Error {
|
|
constructor(
|
|
message: string,
|
|
readonly status: number,
|
|
) {
|
|
super(message);
|
|
this.name = "AttachmentDownloadError";
|
|
}
|
|
}
|
|
|
|
export type RefreshDiscordAttachmentUrl = () => Promise<string | null>;
|
|
|
|
function toErrorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
function shouldRefreshDiscordUrl(error: unknown): boolean {
|
|
return (
|
|
error instanceof AttachmentDownloadError &&
|
|
(error.status === 403 || error.status === 404)
|
|
);
|
|
}
|
|
|
|
export async function uploadAttachmentToTele(
|
|
fileBuffer: Buffer,
|
|
filename: string,
|
|
contentType = "application/octet-stream",
|
|
): Promise<string> {
|
|
try {
|
|
const result = await uploadToTele({
|
|
buffer: fileBuffer,
|
|
filename,
|
|
contentType,
|
|
uploadUrl: config.TELE_UPLOAD_URL,
|
|
timeoutMs: config.ATTACHMENT_UPLOAD_TIMEOUT_MS,
|
|
retries: 0,
|
|
logger,
|
|
});
|
|
|
|
return result.url;
|
|
} catch (error) {
|
|
logger.error(
|
|
{
|
|
filename,
|
|
error: toErrorMessage(error),
|
|
},
|
|
"Failed to upload attachment",
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function downloadDiscordAttachment(url: string): Promise<Buffer> {
|
|
try {
|
|
const response = await fetch(url, {
|
|
signal: AbortSignal.timeout(config.ATTACHMENT_UPLOAD_TIMEOUT_MS),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new AttachmentDownloadError(
|
|
`Download failed with status ${response.status}`,
|
|
response.status,
|
|
);
|
|
}
|
|
|
|
const buffer = await response.arrayBuffer();
|
|
return Buffer.from(buffer);
|
|
} catch (error) {
|
|
logger.error(
|
|
{ url, error: toErrorMessage(error) },
|
|
"Failed to download Discord attachment",
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function processAttachmentUpload(
|
|
attachmentId: string,
|
|
discordUrl: string,
|
|
filename: string,
|
|
options: {
|
|
refreshDiscordUrl?: RefreshDiscordAttachmentUrl;
|
|
contentType?: string;
|
|
} = {},
|
|
): Promise<void> {
|
|
try {
|
|
let currentDiscordUrl = discordUrl;
|
|
let buffer: Buffer;
|
|
try {
|
|
buffer = await downloadDiscordAttachment(currentDiscordUrl);
|
|
} catch (error) {
|
|
if (!options.refreshDiscordUrl || !shouldRefreshDiscordUrl(error)) {
|
|
throw error;
|
|
}
|
|
|
|
const freshUrl = await options.refreshDiscordUrl();
|
|
if (!freshUrl) throw error;
|
|
currentDiscordUrl = freshUrl;
|
|
await updateAttachmentDiscordUrl(attachmentId, freshUrl);
|
|
buffer = await downloadDiscordAttachment(currentDiscordUrl);
|
|
}
|
|
|
|
const sizeMb = buffer.length / (1024 * 1024);
|
|
if (sizeMb > config.ATTACHMENT_MAX_SIZE_MB) {
|
|
throw new Error(
|
|
`File size ${sizeMb.toFixed(2)}MB exceeds limit of ${config.ATTACHMENT_MAX_SIZE_MB}MB`,
|
|
);
|
|
}
|
|
|
|
const uploadedUrl = await uploadAttachmentToTele(
|
|
buffer,
|
|
filename,
|
|
options.contentType,
|
|
);
|
|
|
|
await updateAttachmentAsUploaded(attachmentId, uploadedUrl, Date.now());
|
|
} catch (error) {
|
|
const errorMsg = toErrorMessage(error);
|
|
await updateAttachmentAsFailedUpload(attachmentId, errorMsg);
|
|
logger.error({ attachmentId, error: errorMsg }, "Attachment upload failed");
|
|
}
|
|
}
|