Migrate configuration validation and core moderation types from individual services to the `@bete/shared` package to ensure consistency across the monorepo. - Move `AppConfig` and moderation-related interfaces to `packages/shared`. - Replace service-specific Zod schemas with the centralized shared configuration. - Refactor `services/backend` and `services/discord-gateway` to consume shared config and types. - Remove redundant type definitions and local configuration logic in services. - Update `packages/shared` exports to include new `config` and `moderation-types` modules. - Clean up unused files and deprecated utility functions in `packages/shared`.
133 lines
3.5 KiB
TypeScript
133 lines
3.5 KiB
TypeScript
import { createChildLogger } from "@bete/shared/logger";
|
|
import { config } from "../../shared/config/config.js";
|
|
import {
|
|
updateAttachmentAsFailedUpload,
|
|
updateAttachmentAsUploaded,
|
|
updateAttachmentDiscordUrl,
|
|
} from "../message-capture/messageStore.js";
|
|
import { uploadToTele } from "../voice-recording/teleUpload.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,
|
|
});
|
|
|
|
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");
|
|
}
|
|
}
|