Add group cloud: files index api, web UI, archive sync bot
The Telegram-like group file contract (packages/core shared-files.ts) becomes a working subsystem, built in three parallel tracks: API (NestJS): migration 029 group_files (chat_media_archive metadata-only index with partial replay-unique + RLS, group_file_shares with sha256 token persistence, archive_sync_state/outbox durable-claim seam incl. a unique claim_key index), group-files module (list/search with cursor pagination, direct upload ≤20 MiB streamed+hashed to oCIS WebDAV, download with ACL + retention rechecks, share links with expiry, retention modes incl. legalHold) — RBAC group-membership authz + audit chain events. Web: Files section per group — group picker, drag-drop upload (20 MiB cap), search + MIME/origin filters, cursor pagination, download via authenticated blob fetch, share dialog with expiry + shown-once token, admin retention dialog; bilingual EN/ES (wiki i18n pattern); nav sidebar/palette/bottom-nav wiring; MIME classes aligned to the DB CHECK (document/other). Archive bot (apps/archive-bot): plain sync-loop Matrix bot mirroring attachments from explicitly-mapped unencrypted archive rooms — raw /sync with durable since-cursor, outbox-claimed ingestion (hash-verified stream, no temp files), magic-byte MIME sniffing, safe-name normalization, WebDAV upload, indexed + replied with link, redaction→unavailable; health endpoint; Dockerfile. Tests 74/74, typecheck clean across api/web/bot.
This commit is contained in:
parent
eb1b60f67d
commit
d58db63fe6
58 changed files with 5543 additions and 4 deletions
56
apps/archive-bot/src/config.test.ts
Normal file
56
apps/archive-bot/src/config.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { ConfigError, loadConfig, parseRoomMappings, toGroupFileMapping } from "./config.js";
|
||||
|
||||
const WS = "11111111-1111-4111-8111-111111111111";
|
||||
const GROUP = "22222222-2222-4222-8222-222222222222";
|
||||
|
||||
describe("parseRoomMappings", () => {
|
||||
it("accepts immutable room IDs with UUID workspace/group", () => {
|
||||
expect(parseRoomMappings(JSON.stringify([{ roomId: "!abc:example.org", workspaceId: WS, groupId: GROUP }])))
|
||||
.toEqual([{ roomId: "!abc:example.org", workspaceId: WS, groupId: GROUP }]);
|
||||
});
|
||||
|
||||
it("rejects aliases, non-UUIDs, duplicates, empty and malformed input", () => {
|
||||
expect(() => parseRoomMappings(JSON.stringify([{ roomId: "#archive:example.org", workspaceId: WS, groupId: GROUP }])))
|
||||
.toThrow(ConfigError);
|
||||
expect(() => parseRoomMappings(JSON.stringify([{ roomId: "!a:x", workspaceId: "ws", groupId: GROUP }]))).toThrow(ConfigError);
|
||||
const dup = { roomId: "!a:x", workspaceId: WS, groupId: GROUP };
|
||||
expect(() => parseRoomMappings(JSON.stringify([dup, dup]))).toThrow(/duplicate/);
|
||||
expect(() => parseRoomMappings("[]")).toThrow(ConfigError);
|
||||
expect(() => parseRoomMappings("{nope")).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it("maps to an unencrypted-archive GroupFileMapping kept forever", () => {
|
||||
const mapping = toGroupFileMapping({ roomId: "!a:x", workspaceId: WS, groupId: GROUP });
|
||||
expect(mapping.archivePolicy).toBe("unencrypted-archive");
|
||||
expect(mapping.retention).toEqual({ legalHold: false, mode: "keep" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadConfig", () => {
|
||||
const env = {
|
||||
DATABASE_URL: "postgres://bot@db/hub",
|
||||
MATRIX_HOMESERVER_URL: "https://matrix.example.org",
|
||||
MATRIX_USER: "archive-bot",
|
||||
MATRIX_PASSWORD: "pw",
|
||||
ROOM_MAPPINGS: JSON.stringify([{ roomId: "!a:x", workspaceId: WS, groupId: GROUP }]),
|
||||
FILES_OCID_WEBDAV_URL: "https://files.example.org",
|
||||
FILES_OCID_USER: "svc",
|
||||
FILES_OCID_PASSWORD: "pw2",
|
||||
ARCHIVE_BOT_HEALTH_PORT: "9100",
|
||||
MATRIX_MEDIA_REDIRECT_HOSTS: " R2.example.com ,",
|
||||
};
|
||||
|
||||
it("parses the full env contract", () => {
|
||||
const config = loadConfig(env);
|
||||
expect(config.healthPort).toBe(9100);
|
||||
expect(config.mediaRedirectHosts).toEqual(["r2.example.com"]);
|
||||
expect(config.matrixDeviceId).toBe("HUB_ARCHIVE_BOT");
|
||||
});
|
||||
|
||||
it("fails on missing secrets or a bad port", () => {
|
||||
expect(() => loadConfig({ ...env, MATRIX_PASSWORD: "" })).toThrow(/MATRIX_PASSWORD/);
|
||||
expect(() => loadConfig({ ...env, ARCHIVE_BOT_HEALTH_PORT: "70000" })).toThrow(/PORT/);
|
||||
expect(() => loadConfig({ ...env, FILES_OCID_WEBDAV_URL: "ftp://x" })).toThrow(/http/);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue