T-HUB/apps/archive-bot/src/main.ts

81 lines
3.3 KiB
TypeScript

import { Archiver } from "./archiver.js";
import { loadConfig } from "./config.js";
import { BundleExporter } from "./exporter.js";
import { R2ExportStore } from "./r2-export.js";
import { startHealthServer } from "./health.js";
import { logger } from "./logger.js";
import { MatrixClient } from "./matrix.js";
import { ArchiveProcessor } from "./processor.js";
import { PgArchiveStore } from "./store.js";
import { OcisWebDav } from "./webdav.js";
async function main(): Promise<void> {
const config = loadConfig();
const shutdown = new AbortController();
for (const signal of ["SIGTERM", "SIGINT"] as const) {
process.once(signal, () => {
logger.info({ signal }, "archive-bot: shutting down");
shutdown.abort();
});
}
const matrix = new MatrixClient({
homeserver: config.homeserverUrl,
user: config.matrixUser,
password: config.matrixPassword,
deviceId: config.matrixDeviceId,
mediaRedirectHosts: config.mediaRedirectHosts,
roomIds: config.mappings.map((m) => m.roomId),
logger,
});
await matrix.login(shutdown.signal);
logger.info({ userId: matrix.userId, rooms: config.mappings.length }, "archive-bot: logged in");
const store = PgArchiveStore.create(config.databaseUrl);
const library = new OcisWebDav(config.webdav.baseUrl, config.webdav.user, config.webdav.password);
// Phase-B: encrypted export bundles. Null (fully inert) unless configured.
let exporter: BundleExporter | null = null;
if (config.export !== null) {
if (config.export.r2.endpoint === "") {
logger.warn("archive-export: ARCHIVE_EXPORT_R2_ENDPOINT is not set; exports disabled");
} else {
const upload = new R2ExportStore({
endpoint: config.export.r2.endpoint,
region: config.export.r2.region,
accessKeyId: config.export.r2.accessKeyId,
secretAccessKey: config.export.r2.secretAccessKey,
bucket: config.export.r2.bucket,
spillDir: config.export.r2.tmpDir,
});
exporter = new BundleExporter(store, library, upload, config.export.ageRecipients, config.export.r2.bucket, logger);
logger.info(
{ recipients: config.export.ageRecipients.length, every: config.export.every, bucket: config.export.r2.bucket },
"archive-export: encrypted bundles enabled",
);
}
}
const processor = new ArchiveProcessor(
matrix, store, new Archiver(matrix, library, store), config.mappings, logger,
() => new Date(), exporter, exporter === null ? null : config.export,
);
await processor.init();
// Startup lint: WARN any unencrypted room outside the mapping (policy violation).
await processor.lintEncryption(shutdown.signal).catch((err: unknown) => {
logger.warn({ err: err instanceof Error ? err.message : String(err) }, "archive-bot: encryption lint failed");
});
const health = startHealthServer(config.healthPort, () => processor.health(), logger);
try {
await processor.run(shutdown.signal, (signal) => matrix.login(signal));
} finally {
await new Promise<void>((resolve) => health.close(() => resolve()));
await store.close();
}
logger.info("archive-bot: stopped");
}
void main().catch((error: unknown) => {
logger.fatal({ err: error instanceof Error ? error.message : String(error) }, "archive-bot: fatal");
process.exitCode = 1;
});