From 0e4d6b401cf4fffdcab37d491f0450bfaaf4983c Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 10 Mar 2024 13:57:26 -1000 Subject: [PATCH] Fix media code, clean up old types --- bun.lockb | Bin 424080 -> 427088 bytes bunfig.toml | 2 + classes/configmanager.ts | 446 ------------------ classes/media.ts | 273 ----------- database/entities/Attachment.ts | 15 +- package.json | 5 +- packages/media-manager/backends/local.ts | 4 +- packages/media-manager/backends/s3.ts | 4 +- packages/media-manager/index.ts | 5 +- .../tests/media-backends.test.ts | 2 +- .../v1/accounts/update_credentials/index.ts | 28 +- server/api/api/v1/media/[id]/index.ts | 30 +- server/api/api/v1/media/index.ts | 30 +- server/api/api/v1/profile/avatar.ts | 3 +- server/api/api/v1/profile/header.ts | 3 +- server/api/api/v1/statuses/[id]/context.ts | 3 +- server/api/api/v1/statuses/[id]/favourite.ts | 3 +- .../api/api/v1/statuses/[id]/favourited_by.ts | 3 +- server/api/api/v1/statuses/[id]/index.ts | 3 +- server/api/api/v1/statuses/[id]/pin.ts | 3 +- server/api/api/v1/statuses/[id]/reblog.ts | 3 +- .../api/api/v1/statuses/[id]/reblogged_by.ts | 3 +- server/api/api/v1/statuses/[id]/source.ts | 3 +- .../api/api/v1/statuses/[id]/unfavourite.ts | 3 +- server/api/api/v1/statuses/[id]/unpin.ts | 3 +- server/api/api/v1/statuses/[id]/unreblog.ts | 3 +- server/api/api/v1/statuses/index.ts | 3 +- server/api/api/v1/timelines/home.ts | 3 +- server/api/api/v1/timelines/public.ts | 3 +- server/api/api/v2/media/index.ts | 32 +- server/api/api/v2/search/index.ts | 3 +- utils/api.ts | 5 +- utils/config.ts | 405 ---------------- utils/constants.ts | 4 +- 34 files changed, 137 insertions(+), 1204 deletions(-) create mode 100644 bunfig.toml delete mode 100644 classes/configmanager.ts delete mode 100644 classes/media.ts delete mode 100644 utils/config.ts diff --git a/bun.lockb b/bun.lockb index 04d28da78f7fb0f0032580fef15f663800caf99b..633b6622eaa4d6a79807b118a118c26864ca308c 100755 GIT binary patch delta 85120 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

qo{l5LIuc<>qfEA6k@6)))@GyFd# zq&HeR^Th}2;$!V>=4Oir2u0<$Vrw@4vF4w5a9!q>rAMn=ll|F^LUvJkUi|(bJ`3V4 z@gWJRCEi3;fq0X~Td=OP9Wz-@TcK)+Dp#=h(XOfzAdg&BgDk!Pe$-%3y&(#y!E(DR z=oQZXeFLy#Ail*-z+VMwf1>TW_`Y8)B&<9qAG7^?(Vd4c}~ zzNv|iIN2tsv(p4dF^Tz))cdB|G4Px7Kgwm`AK*wQc#2(!_lK*+;uYeaHI|ZJRUTlP z`lo~VmJn~sKlWY5X?B#yx0;fIN)3U)Rb6ccf1C|>o@%GzAGerY#L?j9U=&Ew{NI2> zP1Yve*07S(VD`j7pfSzb|3=jX7g-$0EiRstJ)s~rf$-vcyLc~=j_wogLEE7lPq$md zov0S3W$3Z!&I~(dF2z2wJ>YU8NCQ1>z;UR?!lymFk?p%xozu&ot*Rc1qci3CZ|~Ip?j2eDhU<^_ zD^F74R)H3}aanq8@)M!N@W^BD?0iwWeKW$xh68~SzN-w^K)FIMhrNb=(coTQ=l;FI>3anN=LiR{_ZDM+g+1Dj za7Ih+5xgb+Gr|w!8s-)D@6iIsSRZ92@+Qv@wh-5NpS55>S#}OC%a-D@CHW1Pje8z_ zIL?o;99OpQYTGJa&PljN`sG~J%F7=Vjf5DMr_=Czd8Oy1g?nIi^9s+&2$p%B2lom+ zx{sGXI2!tSAFpO`GD=^vo|b3c}0@gz+?rz+F|L57{h$8+M#KYJFy00weV7h zrG-DmI?Y=$G$VM3*Lira;9xIrc&|t$N3TJC^wQyJ;is|scqzj&LcbK9V!*^YSYq0NnEADDHkj4FC(x+p! zdNy5$c{9)I7^-jM)tp7^ac`nx2fft6X~7X*=geNgo4vfuUZM97_G&Vt!K1w$nY|*p z+?w=IT7Jc!z;bo%pB4;xi%0f~oWkr?H$=R~!D*o<+In5ijz$uh;wSsCmVoKr;;ck-mq=!_o)AtGpfO_6ogmxYuP=G`O#qH>wx? zzj#!y$gLbPN04O5Ys^ZEe1)a6=|0LJbnKB{m-C|G3%Jee@1>mAqXmxMI5Hxi;5v

7*Dy93Ds1m{IX@b%Z665qRQHFzZ12^a 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 diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000..bea1efe1 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[install.scopes] +"@jsr" = "https://npm.jsr.io" diff --git a/classes/configmanager.ts b/classes/configmanager.ts deleted file mode 100644 index 2007bbed..00000000 --- a/classes/configmanager.ts +++ /dev/null @@ -1,446 +0,0 @@ -/** - * @file configmanager.ts - * @summary ConfigManager system to retrieve and modify system configuration - * @description Can read from a hand-written file, config.toml, or from a machine-saved file, config.internal.toml - * @deprecated Use the new ConfigManager class instead - * Fuses both and provides a way to retrieve individual values - */ - -/* import { parse, stringify } from "@iarna/toml"; -import chalk from "chalk"; -import merge from "merge-deep-ts"; - -const scanConfig = async () => { - const config = Bun.file(process.cwd() + "/config/config.toml"); - - if (!(await config.exists())) { - console.error( - `${chalk.red(`✗`)} ${chalk.bold( - "Error while reading config: " - )} Config file not found` - ); - process.exit(1); - } - - return parse(await config.text()) as ConfigType; -}; */ - -// Creates the internal config with nothing in it if it doesnt exist -/* const scanInternalConfig = async () => { - const config = Bun.file(process.cwd() + "/config/config.internal.toml"); - - if (!(await config.exists())) { - await Bun.write(config, ""); - } - - return parse(await config.text()) as ConfigType; -}; - -let config = await scanConfig(); -const internalConfig = await scanInternalConfig(); - -export interface ConfigType { - database: { - host: string; - port: number; - username: string; - password: string; - database: string; - }; - - redis: { - queue: { - host: string; - port: number; - password: string; - database: number | null; - }; - cache: { - host: string; - port: number; - password: string; - database: number | null; - enabled: boolean; - }; - }; - - meilisearch: { - host: string; - port: number; - api_key: string; - enabled: boolean; - }; - - signups: { - tos_url: string; - rules: string[]; - registration: boolean; - }; - - oidc: { - providers: { - name: string; - id: string; - url: string; - client_id: string; - client_secret: string; - icon: string; - }[]; - }; - - http: { - base_url: string; - bind: string; - bind_port: string; - banned_ips: string[]; - banned_user_agents: string[]; - }; - - instance: { - name: string; - description: string; - banner: string; - logo: string; - }; - - smtp: { - server: string; - port: number; - username: string; - password: string; - tls: boolean; - }; - - validation: { - max_displayname_size: number; - max_bio_size: number; - max_username_size: number; - max_note_size: number; - max_avatar_size: number; - max_header_size: number; - max_media_size: number; - max_media_attachments: number; - max_media_description_size: number; - max_poll_options: number; - max_poll_option_size: number; - min_poll_duration: number; - max_poll_duration: number; - - username_blacklist: string[]; - blacklist_tempmail: boolean; - email_blacklist: string[]; - url_scheme_whitelist: string[]; - - enforce_mime_types: boolean; - allowed_mime_types: string[]; - }; - - media: { - backend: string; - deduplicate_media: boolean; - conversion: { - convert_images: boolean; - convert_to: string; - }; - }; - - s3: { - endpoint: string; - access_key: string; - secret_access_key: string; - region: string; - bucket_name: string; - public_url: string; - }; - - defaults: { - visibility: string; - language: string; - avatar: string; - header: string; - }; - - email: { - send_on_report: boolean; - send_on_suspend: boolean; - send_on_unsuspend: boolean; - }; - - activitypub: { - use_tombstones: boolean; - reject_activities: string[]; - force_followers_only: string[]; - discard_reports: string[]; - discard_deletes: string[]; - discard_banners: string[]; - discard_avatars: string[]; - discard_updates: string[]; - discard_follows: string[]; - force_sensitive: string[]; - remove_media: string[]; - fetch_all_collection_members: boolean; - authorized_fetch: boolean; - }; - - filters: { - note_filters: string[]; - username_filters: string[]; - displayname_filters: string[]; - bio_filters: string[]; - emoji_filters: string[]; - }; - - logging: { - log_requests: boolean; - log_requests_verbose: boolean; - log_filters: boolean; - }; - - ratelimits: { - duration_coeff: number; - max_coeff: number; - }; - - custom_ratelimits: Record< - string, - { - duration: number; - max: number; - } - >; - [key: string]: unknown; -} - -export const configDefaults: ConfigType = { - http: { - bind: "http://0.0.0.0", - bind_port: "8000", - base_url: "http://lysand.localhost:8000", - banned_ips: [], - banned_user_agents: [], - }, - database: { - host: "localhost", - port: 5432, - username: "postgres", - password: "postgres", - database: "lysand", - }, - redis: { - queue: { - host: "localhost", - port: 6379, - password: "", - database: 0, - }, - cache: { - host: "localhost", - port: 6379, - password: "", - database: 1, - enabled: false, - }, - }, - meilisearch: { - host: "localhost", - port: 1491, - api_key: "", - enabled: false, - }, - signups: { - tos_url: "", - rules: [], - registration: false, - }, - oidc: { - providers: [], - }, - instance: { - banner: "", - description: "", - logo: "", - name: "", - }, - smtp: { - password: "", - port: 465, - server: "", - tls: true, - username: "", - }, - media: { - backend: "local", - deduplicate_media: true, - conversion: { - convert_images: false, - convert_to: "webp", - }, - }, - email: { - send_on_report: false, - send_on_suspend: false, - send_on_unsuspend: false, - }, - s3: { - access_key: "", - bucket_name: "", - endpoint: "", - public_url: "", - region: "", - secret_access_key: "", - }, - validation: { - max_displayname_size: 50, - max_bio_size: 6000, - max_note_size: 5000, - max_avatar_size: 5_000_000, - max_header_size: 5_000_000, - max_media_size: 40_000_000, - max_media_attachments: 10, - max_media_description_size: 1000, - max_poll_options: 20, - max_poll_option_size: 500, - min_poll_duration: 60, - max_poll_duration: 1893456000, - max_username_size: 30, - - username_blacklist: [ - ".well-known", - "~", - "about", - "activities", - "api", - "auth", - "dev", - "inbox", - "internal", - "main", - "media", - "nodeinfo", - "notice", - "oauth", - "objects", - "proxy", - "push", - "registration", - "relay", - "settings", - "status", - "tag", - "users", - "web", - "search", - "mfa", - ], - - blacklist_tempmail: false, - - email_blacklist: [], - - url_scheme_whitelist: [ - "http", - "https", - "ftp", - "dat", - "dweb", - "gopher", - "hyper", - "ipfs", - "ipns", - "irc", - "xmpp", - "ircs", - "magnet", - "mailto", - "mumble", - "ssb", - ], - - enforce_mime_types: false, - allowed_mime_types: [], - }, - defaults: { - visibility: "public", - language: "en", - avatar: "", - header: "", - }, - activitypub: { - use_tombstones: true, - reject_activities: [], - force_followers_only: [], - discard_reports: [], - discard_deletes: [], - discard_banners: [], - discard_avatars: [], - force_sensitive: [], - discard_updates: [], - discard_follows: [], - remove_media: [], - fetch_all_collection_members: false, - authorized_fetch: false, - }, - filters: { - note_filters: [], - username_filters: [], - displayname_filters: [], - bio_filters: [], - emoji_filters: [], - }, - logging: { - log_requests: false, - log_requests_verbose: false, - log_filters: true, - }, - ratelimits: { - duration_coeff: 1, - max_coeff: 1, - }, - custom_ratelimits: {}, -}; */ - -/* export const getConfig = () => { - // Deeply merge configDefaults, config and internalConfig - return merge([configDefaults, config, internalConfig]) as any as ConfigType; -}; - */ -/** - * Sets the internal config - * @param newConfig Any part of ConfigType - */ -/* export const setConfig = async (newConfig: Partial) => { - const newInternalConfig = merge([ - internalConfig, - newConfig, - ]) as any as ConfigType; - - // Prepend a warning comment and write the new TOML to the file - await Bun.write( - Bun.file(process.cwd() + "/config/config.internal.toml"), - `# This file is automatically generated. Do not modify it manually.\n${stringify( - newInternalConfig as any - )}` - ); -}; */ - -/* export const getHost = () => { - const url = new URL(getConfig().http.base_url); - - return url.host; -}; - */ -// Refresh config every 5 seconds -/* setInterval(() => { - scanConfig() - .then(newConfig => { - if (newConfig !== config) { - config = newConfig; - } - }) - .catch(e => { - console.error(e); - }); -}, 5000); */ - -/* export { config }; - */ diff --git a/classes/media.ts b/classes/media.ts deleted file mode 100644 index 483a173d..00000000 --- a/classes/media.ts +++ /dev/null @@ -1,273 +0,0 @@ -import type { GetObjectCommandOutput } from "@aws-sdk/client-s3"; -import { - GetObjectCommand, - PutObjectCommand, - S3Client, -} from "@aws-sdk/client-s3"; -import type { ConfigType } from "~classes/configmanager"; -import sharp from "sharp"; -import { exists, mkdir } from "fs/promises"; -class MediaBackend { - backend: string; - - constructor(backend: string) { - this.backend = backend; - } - - /** - * Adds media to the media backend - * @param media - * @returns The hash of the file in SHA-256 (hex format) with the file extension added to it - */ - async addMedia(media: File) { - const hash = new Bun.SHA256() - .update(await media.arrayBuffer()) - .digest("hex"); - - return `${hash}.${media.name.split(".").pop()}`; - } - - async convertMedia(media: File, config: ConfigType) { - const sharpCommand = sharp(await media.arrayBuffer()); - - // Rename ".jpg" files to ".jpeg" to avoid sharp errors - let name = media.name; - if (media.name.endsWith(".jpg")) { - name = media.name.replace(".jpg", ".jpeg"); - } - - const fileFormatToConvertTo = config.media.conversion.convert_to; - - switch (fileFormatToConvertTo) { - case "png": - return new File( - [(await sharpCommand.png().toBuffer()).buffer] as any, - // Replace the file extension with PNG - name.replace(/\.[^/.]+$/, ".png"), - { - type: "image/png", - } - ); - case "webp": - return new File( - [(await sharpCommand.webp().toBuffer()).buffer] as any, - // Replace the file extension with WebP - name.replace(/\.[^/.]+$/, ".webp"), - { - type: "image/webp", - } - ); - case "jpeg": - return new File( - [(await sharpCommand.jpeg().toBuffer()).buffer] as any, - // Replace the file extension with JPEG - name.replace(/\.[^/.]+$/, ".jpeg"), - { - type: "image/jpeg", - } - ); - case "avif": - return new File( - [(await sharpCommand.avif().toBuffer()).buffer] as any, - // Replace the file extension with AVIF - name.replace(/\.[^/.]+$/, ".avif"), - { - type: "image/avif", - } - ); - // Needs special build of libvips - case "jxl": - return new File( - [(await sharpCommand.jxl().toBuffer()).buffer] as any, - // Replace the file extension with JXL - name.replace(/\.[^/.]+$/, ".jxl"), - { - type: "image/jxl", - } - ); - case "heif": - return new File( - [(await sharpCommand.heif().toBuffer()).buffer] as any, - // Replace the file extension with HEIF - name.replace(/\.[^/.]+$/, ".heif"), - { - type: "image/heif", - } - ); - default: - return media; - } - } - - /** - * Retrieves element from media backend by hash - * @param hash The hash of the element in SHA-256 hex format - * @param extension The extension of the file - * @returns The file as a File object - */ - // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars - async getMediaByHash( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hash: string - ): Promise { - return new File([], "test"); - } -} - -/** - * S3 Backend, stores files in S3 - */ -export class S3Backend extends MediaBackend { - client: S3Client; - config: ConfigType; - - constructor(config: ConfigType) { - super("s3"); - - this.config = config; - - this.client = new S3Client({ - endpoint: this.config.s3.endpoint, - region: this.config.s3.region || "auto", - credentials: { - accessKeyId: this.config.s3.access_key, - secretAccessKey: this.config.s3.secret_access_key, - }, - }); - } - - async addMedia(media: File): Promise { - if (this.config.media.conversion.convert_images) { - media = await this.convertMedia(media, this.config); - } - - const hash = await super.addMedia(media); - - if (!hash) { - throw new Error("Failed to hash file"); - } - - // Check if file is already present - const existingFile = await this.getMediaByHash(hash); - - if (existingFile) { - // File already exists, so return the hash without uploading it - return hash; - } - - const command = new PutObjectCommand({ - Bucket: this.config.s3.bucket_name, - Key: hash, - Body: Buffer.from(await media.arrayBuffer()), - ContentType: media.type, - ContentLength: media.size, - Metadata: { - "x-amz-meta-original-name": media.name, - }, - }); - - const response = await this.client.send(command); - - if (response.$metadata.httpStatusCode !== 200) { - throw new Error("Failed to upload file"); - } - - return hash; - } - - async getMediaByHash(hash: string): Promise { - const command = new GetObjectCommand({ - Bucket: this.config.s3.bucket_name, - Key: hash, - }); - - let response: GetObjectCommandOutput; - - try { - response = await this.client.send(command); - } catch { - return null; - } - - if (response.$metadata.httpStatusCode !== 200) { - throw new Error("Failed to get file"); - } - - const body = await response.Body?.transformToByteArray(); - - if (!body) { - throw new Error("Failed to get file"); - } - - return new File([body], hash, { - type: response.ContentType, - }); - } -} - -/** - * Local backend, stores files on filesystem - */ -export class LocalBackend extends MediaBackend { - config: ConfigType; - - constructor(config: ConfigType) { - super("local"); - - this.config = config; - } - - async addMedia(media: File): Promise { - if (this.config.media.conversion.convert_images) { - media = await this.convertMedia(media, this.config); - } - - const hash = await super.addMedia(media); - - if (!(await exists(`${process.cwd()}/uploads`))) { - await mkdir(`${process.cwd()}/uploads`); - } - - await Bun.write(Bun.file(`${process.cwd()}/uploads/${hash}`), media); - - return hash; - } - - async getMediaByHash(hash: string): Promise { - const file = Bun.file(`${process.cwd()}/uploads/${hash}`); - - if (!(await file.exists())) { - return null; - } - - return new File([await file.arrayBuffer()], `${hash}`, { - type: file.type, - }); - } -} - -export const uploadFile = (file: File, config: ConfigType) => { - const backend = config.media.backend; - - if (backend === "local") { - return new LocalBackend(config).addMedia(file); - } else if (backend === "s3") { - return new S3Backend(config).addMedia(file); - } -}; - -export const getFile = ( - hash: string, - extension: string, - config: ConfigType -) => { - const backend = config.media.backend; - - if (backend === "local") { - return new LocalBackend(config).getMediaByHash(hash); - } else if (backend === "s3") { - return new S3Backend(config).getMediaByHash(hash); - } - - return null; -}; diff --git a/database/entities/Attachment.ts b/database/entities/Attachment.ts index fefa8cd7..abb07c9e 100644 --- a/database/entities/Attachment.ts +++ b/database/entities/Attachment.ts @@ -1,5 +1,6 @@ -import type { ConfigType } from "~classes/configmanager"; import type { Attachment } from "@prisma/client"; +import type { ConfigType } from "config-manager"; +import { MediaBackendType } from "media-manager"; import type { APIAsyncAttachment } from "~types/entities/async_attachment"; import type { APIAttachment } from "~types/entities/attachment"; @@ -56,11 +57,13 @@ export const attachmentToAPI = ( }; }; -export const getUrl = (hash: string, config: ConfigType) => { - if (config.media.backend === "local") { - return `${config.http.base_url}/media/${hash}`; - } else if (config.media.backend === "s3") { - return `${config.s3.public_url}/${hash}`; +export const getUrl = (name: string, config: ConfigType) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + if (config.media.backend === MediaBackendType.LOCAL) { + return `${config.http.base_url}/media/${name}`; + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + } else if (config.media.backend === MediaBackendType.S3) { + return `${config.s3.public_url}/${name}`; } return ""; }; diff --git a/package.json b/package.json index f1ef982e..ea00043a 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,9 @@ "semver": "^7.5.4", "sharp": "^0.33.0-rc.2", "request-parser": "file:packages/request-parser", - "config-manager": "file:packages/config-manager" + "config-manager": "file:packages/config-manager", + "cli-parser": "file:packages/cli-parser", + "log-manager": "file:packages/log-manager", + "media-manager": "file:packages/media-manager" } } \ No newline at end of file diff --git a/packages/media-manager/backends/local.ts b/packages/media-manager/backends/local.ts index 5c955b84..d5a8fb99 100644 --- a/packages/media-manager/backends/local.ts +++ b/packages/media-manager/backends/local.ts @@ -4,8 +4,8 @@ import { MediaBackend, MediaBackendType, MediaHasher } from ".."; import type { ConfigType } from "config-manager"; export class LocalMediaBackend extends MediaBackend { - constructor(private config: ConfigType) { - super(MediaBackendType.LOCAL); + constructor(config: ConfigType) { + super(config, MediaBackendType.LOCAL); } public async addFile(file: File) { diff --git a/packages/media-manager/backends/s3.ts b/packages/media-manager/backends/s3.ts index 8098e2f2..46c2cb41 100644 --- a/packages/media-manager/backends/s3.ts +++ b/packages/media-manager/backends/s3.ts @@ -6,7 +6,7 @@ import type { ConfigType } from "config-manager"; export class S3MediaBackend extends MediaBackend { constructor( - private config: ConfigType, + config: ConfigType, private s3Client = new S3Client({ endPoint: config.s3.endpoint, useSSL: true, @@ -16,7 +16,7 @@ export class S3MediaBackend extends MediaBackend { secretKey: config.s3.secret_access_key, }) ) { - super(MediaBackendType.S3); + super(config, MediaBackendType.S3); } public async addFile(file: File) { diff --git a/packages/media-manager/index.ts b/packages/media-manager/index.ts index 77c2f581..501495b1 100644 --- a/packages/media-manager/index.ts +++ b/packages/media-manager/index.ts @@ -27,7 +27,10 @@ export class MediaHasher { } export class MediaBackend { - constructor(private backend: MediaBackendType) {} + constructor( + public config: ConfigType, + public backend: MediaBackendType + ) {} public getBackendType() { return this.backend; diff --git a/packages/media-manager/tests/media-backends.test.ts b/packages/media-manager/tests/media-backends.test.ts index bad3ac5e..fc36068f 100644 --- a/packages/media-manager/tests/media-backends.test.ts +++ b/packages/media-manager/tests/media-backends.test.ts @@ -16,7 +16,6 @@ describe("MediaBackend", () => { let mockConfig: ConfigType; beforeEach(() => { - mediaBackend = new MediaBackend(MediaBackendType.S3); mockConfig = { media: { conversion: { @@ -24,6 +23,7 @@ describe("MediaBackend", () => { }, }, } as ConfigType; + mediaBackend = new MediaBackend(mockConfig, MediaBackendType.S3); }); it("should initialize with correct backend type", () => { diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index fc66d92c..4a48807f 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -3,12 +3,16 @@ import { userRelations, userToAPI } from "~database/entities/User"; import { apiRoute, applyConfig } from "@api"; import { sanitize } from "isomorphic-dompurify"; import { sanitizeHtml } from "@sanitization"; -import { uploadFile } from "~classes/media"; import ISO6391 from "iso-639-1"; import { parseEmojis } from "~database/entities/Emoji"; import { client } from "~database/datasource"; import type { APISource } from "~types/entities/source"; import { convertTextToHtml } from "@formatting"; +import { MediaBackendType } from "media-manager"; +import type { MediaBackend } from "media-manager"; +import { LocalMediaBackend } from "~packages/media-manager/backends/local"; +import { S3MediaBackend } from "~packages/media-manager/backends/s3"; +import { getUrl } from "~database/entities/Attachment"; export const meta = applyConfig({ allowedMethods: ["PATCH"], @@ -69,6 +73,20 @@ export default apiRoute<{ }; } + let mediaManager: MediaBackend; + + switch (config.media.backend as MediaBackendType) { + case MediaBackendType.LOCAL: + mediaManager = new LocalMediaBackend(config); + break; + case MediaBackendType.S3: + mediaManager = new S3MediaBackend(config); + break; + default: + // TODO: Replace with logger + throw new Error("Invalid media backend"); + } + if (display_name) { // Check if within allowed display name lengths if ( @@ -167,9 +185,9 @@ export default apiRoute<{ ); } - const hash = await uploadFile(avatar, config); + const { uploadedFile } = await mediaManager.addFile(avatar); - user.avatar = hash || ""; + user.avatar = getUrl(uploadedFile.name, config); } if (header) { @@ -181,9 +199,9 @@ export default apiRoute<{ ); } - const hash = await uploadFile(header, config); + const { uploadedFile } = await mediaManager.addFile(header); - user.header = hash || ""; + user.header = getUrl(uploadedFile.name, config); } if (locked) { diff --git a/server/api/api/v1/media/[id]/index.ts b/server/api/api/v1/media/[id]/index.ts index bec0e978..8abd1bf0 100644 --- a/server/api/api/v1/media/[id]/index.ts +++ b/server/api/api/v1/media/[id]/index.ts @@ -1,11 +1,13 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; -import type { APIRouteMeta } from "~types/api"; -import { uploadFile } from "~classes/media"; import { attachmentToAPI, getUrl } from "~database/entities/Attachment"; +import type { MediaBackend } from "media-manager"; +import { MediaBackendType } from "media-manager"; +import { LocalMediaBackend } from "~packages/media-manager/backends/local"; +import { S3MediaBackend } from "~packages/media-manager/backends/s3"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["GET", "PUT"], ratelimits: { max: 10, @@ -61,13 +63,23 @@ export default apiRoute<{ let thumbnailUrl = attachment.thumbnail_url; - if (thumbnail) { - const hash = await uploadFile( - thumbnail as unknown as File, - config - ); + let mediaManager: MediaBackend; - thumbnailUrl = hash ? getUrl(hash, config) : ""; + switch (config.media.backend as MediaBackendType) { + case MediaBackendType.LOCAL: + mediaManager = new LocalMediaBackend(config); + break; + case MediaBackendType.S3: + mediaManager = new S3MediaBackend(config); + break; + default: + // TODO: Replace with logger + throw new Error("Invalid media backend"); + } + + if (thumbnail) { + const { uploadedFile } = await mediaManager.addFile(thumbnail); + thumbnailUrl = getUrl(uploadedFile.name, config); } const descriptionText = description || attachment.description; diff --git a/server/api/api/v1/media/index.ts b/server/api/api/v1/media/index.ts index 0119a7d4..202e5dbb 100644 --- a/server/api/api/v1/media/index.ts +++ b/server/api/api/v1/media/index.ts @@ -2,12 +2,14 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { encode } from "blurhash"; -import type { APIRouteMeta } from "~types/api"; import sharp from "sharp"; -import { uploadFile } from "~classes/media"; import { attachmentToAPI, getUrl } from "~database/entities/Attachment"; +import { MediaBackendType } from "media-manager"; +import type { MediaBackend } from "media-manager"; +import { LocalMediaBackend } from "~packages/media-manager/backends/local"; +import { S3MediaBackend } from "~packages/media-manager/backends/s3"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 10, @@ -88,16 +90,30 @@ export default apiRoute<{ let url = ""; - const hash = await uploadFile(file, config); + let mediaManager: MediaBackend; - url = hash ? getUrl(hash, config) : ""; + switch (config.media.backend as MediaBackendType) { + case MediaBackendType.LOCAL: + mediaManager = new LocalMediaBackend(config); + break; + case MediaBackendType.S3: + mediaManager = new S3MediaBackend(config); + break; + default: + // TODO: Replace with logger + throw new Error("Invalid media backend"); + } + + const { uploadedFile } = await mediaManager.addFile(file); + + url = getUrl(uploadedFile.name, config); let thumbnailUrl = ""; if (thumbnail) { - const hash = await uploadFile(thumbnail as unknown as File, config); + const { uploadedFile } = await mediaManager.addFile(thumbnail); - thumbnailUrl = hash ? getUrl(hash, config) : ""; + thumbnailUrl = getUrl(uploadedFile.name, config); } const newAttachment = await client.attachment.create({ diff --git a/server/api/api/v1/profile/avatar.ts b/server/api/api/v1/profile/avatar.ts index f65ce264..f6a1ea47 100644 --- a/server/api/api/v1/profile/avatar.ts +++ b/server/api/api/v1/profile/avatar.ts @@ -2,9 +2,8 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { userRelations, userToAPI } from "~database/entities/User"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["DELETE"], ratelimits: { max: 10, diff --git a/server/api/api/v1/profile/header.ts b/server/api/api/v1/profile/header.ts index 6a53e6d5..b3b52a60 100644 --- a/server/api/api/v1/profile/header.ts +++ b/server/api/api/v1/profile/header.ts @@ -2,9 +2,8 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { userRelations, userToAPI } from "~database/entities/User"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["DELETE"], ratelimits: { max: 10, diff --git a/server/api/api/v1/statuses/[id]/context.ts b/server/api/api/v1/statuses/[id]/context.ts index 518b9493..93eed932 100644 --- a/server/api/api/v1/statuses/[id]/context.ts +++ b/server/api/api/v1/statuses/[id]/context.ts @@ -7,9 +7,8 @@ import { statusAndUserRelations, statusToAPI, } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["GET"], ratelimits: { max: 8, diff --git a/server/api/api/v1/statuses/[id]/favourite.ts b/server/api/api/v1/statuses/[id]/favourite.ts index 8557a7b2..c58e959e 100644 --- a/server/api/api/v1/statuses/[id]/favourite.ts +++ b/server/api/api/v1/statuses/[id]/favourite.ts @@ -8,10 +8,9 @@ import { statusAndUserRelations, statusToAPI, } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; import type { APIStatus } from "~types/entities/status"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/[id]/favourited_by.ts b/server/api/api/v1/statuses/[id]/favourited_by.ts index 3d61969f..066eb681 100644 --- a/server/api/api/v1/statuses/[id]/favourited_by.ts +++ b/server/api/api/v1/statuses/[id]/favourited_by.ts @@ -6,9 +6,8 @@ import { statusAndUserRelations, } from "~database/entities/Status"; import { userRelations, userToAPI } from "~database/entities/User"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["GET"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/[id]/index.ts b/server/api/api/v1/statuses/[id]/index.ts index d0fae956..814dcca1 100644 --- a/server/api/api/v1/statuses/[id]/index.ts +++ b/server/api/api/v1/statuses/[id]/index.ts @@ -9,9 +9,8 @@ import { statusAndUserRelations, statusToAPI, } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["GET", "DELETE", "PUT"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/[id]/pin.ts b/server/api/api/v1/statuses/[id]/pin.ts index 1d3a2e56..fd215f46 100644 --- a/server/api/api/v1/statuses/[id]/pin.ts +++ b/server/api/api/v1/statuses/[id]/pin.ts @@ -3,9 +3,8 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { statusAndUserRelations, statusToAPI } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/[id]/reblog.ts b/server/api/api/v1/statuses/[id]/reblog.ts index b93846ed..73b0ef73 100644 --- a/server/api/api/v1/statuses/[id]/reblog.ts +++ b/server/api/api/v1/statuses/[id]/reblog.ts @@ -8,9 +8,8 @@ import { statusToAPI, } from "~database/entities/Status"; import { type UserWithRelations } from "~database/entities/User"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/[id]/reblogged_by.ts b/server/api/api/v1/statuses/[id]/reblogged_by.ts index 9af70f36..b02a02a2 100644 --- a/server/api/api/v1/statuses/[id]/reblogged_by.ts +++ b/server/api/api/v1/statuses/[id]/reblogged_by.ts @@ -6,9 +6,8 @@ import { statusAndUserRelations, } from "~database/entities/Status"; import { userRelations, userToAPI } from "~database/entities/User"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["GET"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/[id]/source.ts b/server/api/api/v1/statuses/[id]/source.ts index 02d3fe27..5bb90600 100644 --- a/server/api/api/v1/statuses/[id]/source.ts +++ b/server/api/api/v1/statuses/[id]/source.ts @@ -5,9 +5,8 @@ import { isViewableByUser, statusAndUserRelations, } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["GET"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/[id]/unfavourite.ts b/server/api/api/v1/statuses/[id]/unfavourite.ts index 7d02a30d..66ebe82b 100644 --- a/server/api/api/v1/statuses/[id]/unfavourite.ts +++ b/server/api/api/v1/statuses/[id]/unfavourite.ts @@ -8,10 +8,9 @@ import { statusAndUserRelations, statusToAPI, } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; import type { APIStatus } from "~types/entities/status"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/[id]/unpin.ts b/server/api/api/v1/statuses/[id]/unpin.ts index 8d10af6a..89ac2da4 100644 --- a/server/api/api/v1/statuses/[id]/unpin.ts +++ b/server/api/api/v1/statuses/[id]/unpin.ts @@ -2,9 +2,8 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { statusAndUserRelations, statusToAPI } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/[id]/unreblog.ts b/server/api/api/v1/statuses/[id]/unreblog.ts index 78179bc1..07d92b33 100644 --- a/server/api/api/v1/statuses/[id]/unreblog.ts +++ b/server/api/api/v1/statuses/[id]/unreblog.ts @@ -6,10 +6,9 @@ import { statusAndUserRelations, statusToAPI, } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; import type { APIStatus } from "~types/entities/status"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 100, diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index c51f5256..97eb34e5 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -11,9 +11,8 @@ import { statusToAPI, } from "~database/entities/Status"; import type { UserWithRelations } from "~database/entities/User"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 300, diff --git a/server/api/api/v1/timelines/home.ts b/server/api/api/v1/timelines/home.ts index 014d6f28..ce4e96aa 100644 --- a/server/api/api/v1/timelines/home.ts +++ b/server/api/api/v1/timelines/home.ts @@ -2,9 +2,8 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { statusAndUserRelations, statusToAPI } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["GET"], ratelimits: { max: 200, diff --git a/server/api/api/v1/timelines/public.ts b/server/api/api/v1/timelines/public.ts index 62c3b72f..c58cfb15 100644 --- a/server/api/api/v1/timelines/public.ts +++ b/server/api/api/v1/timelines/public.ts @@ -2,9 +2,8 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { statusAndUserRelations, statusToAPI } from "~database/entities/Status"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["GET"], ratelimits: { max: 200, diff --git a/server/api/api/v2/media/index.ts b/server/api/api/v2/media/index.ts index eeadcbc6..2593ae05 100644 --- a/server/api/api/v2/media/index.ts +++ b/server/api/api/v2/media/index.ts @@ -2,12 +2,14 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { encode } from "blurhash"; -import type { APIRouteMeta } from "~types/api"; import sharp from "sharp"; -import { uploadFile } from "~classes/media"; import { attachmentToAPI, getUrl } from "~database/entities/Attachment"; +import type { MediaBackend } from "media-manager"; +import { MediaBackendType } from "media-manager"; +import { LocalMediaBackend } from "~packages/media-manager/backends/local"; +import { S3MediaBackend } from "~packages/media-manager/backends/s3"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 10, @@ -88,18 +90,32 @@ export default apiRoute<{ let url = ""; - if (isImage) { - const hash = await uploadFile(file, config); + let mediaManager: MediaBackend; - url = hash ? getUrl(hash, config) : ""; + switch (config.media.backend as MediaBackendType) { + case MediaBackendType.LOCAL: + mediaManager = new LocalMediaBackend(config); + break; + case MediaBackendType.S3: + mediaManager = new S3MediaBackend(config); + break; + default: + // TODO: Replace with logger + throw new Error("Invalid media backend"); + } + + if (isImage) { + const { uploadedFile } = await mediaManager.addFile(file); + + url = getUrl(uploadedFile.name, config); } let thumbnailUrl = ""; if (thumbnail) { - const hash = await uploadFile(thumbnail as unknown as File, config); + const { uploadedFile } = await mediaManager.addFile(thumbnail); - thumbnailUrl = hash ? getUrl(hash, config) : ""; + thumbnailUrl = getUrl(uploadedFile.name, config); } const newAttachment = await client.attachment.create({ diff --git a/server/api/api/v2/search/index.ts b/server/api/api/v2/search/index.ts index 6a4d4a9c..c327dedb 100644 --- a/server/api/api/v2/search/index.ts +++ b/server/api/api/v2/search/index.ts @@ -4,9 +4,8 @@ import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { statusAndUserRelations, statusToAPI } from "~database/entities/Status"; import { userRelations, userToAPI } from "~database/entities/User"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["GET"], ratelimits: { max: 10, diff --git a/utils/api.ts b/utils/api.ts index a30ea4ed..283cf136 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -1,9 +1,10 @@ -import { getConfig } from "~classes/configmanager"; +import { ConfigManager } from "config-manager"; import type { RouteHandler } from "~server/api/routes.type"; import type { APIRouteMeta } from "~types/api"; +const config = await new ConfigManager({}).getConfig(); + export const applyConfig = (routeMeta: APIRouteMeta) => { - const config = getConfig(); const newMeta = routeMeta; // Apply ratelimits from config diff --git a/utils/config.ts b/utils/config.ts deleted file mode 100644 index a672a613..00000000 --- a/utils/config.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { parse } from "@iarna/toml"; -import chalk from "chalk"; - -const scanConfig = async () => { - const config = Bun.file(process.cwd() + "/config/config.toml"); - - if (!(await config.exists())) { - console.error( - `${chalk.red(`✗`)} ${chalk.bold( - "Error while reading config: " - )} Config file not found` - ); - process.exit(1); - } - - return parse(await config.text()) as ConfigType; -}; - -let config = await scanConfig(); - -export interface ConfigType { - database: { - host: string; - port: number; - username: string; - password: string; - database: string; - }; - - redis: { - queue: { - host: string; - port: number; - password: string; - database: number | null; - }; - cache: { - host: string; - port: number; - password: string; - database: number | null; - enabled: boolean; - }; - }; - - meilisearch: { - host: string; - port: number; - api_key: string; - enabled: boolean; - }; - - signups: { - tos_url: string; - rules: string[]; - registration: boolean; - }; - - oidc: { - providers: { - name: string; - id: string; - url: string; - client_id: string; - client_secret: string; - icon: string; - }[]; - }; - - http: { - base_url: string; - bind: string; - bind_port: string; - banned_ips: string[]; - }; - - instance: { - name: string; - description: string; - banner: string; - logo: string; - }; - - smtp: { - server: string; - port: number; - username: string; - password: string; - tls: boolean; - }; - - validation: { - max_displayname_size: number; - max_bio_size: number; - max_username_size: number; - max_note_size: number; - max_avatar_size: number; - max_header_size: number; - max_media_size: number; - max_media_attachments: number; - max_media_description_size: number; - max_poll_options: number; - max_poll_option_size: number; - min_poll_duration: number; - max_poll_duration: number; - - username_blacklist: string[]; - blacklist_tempmail: boolean; - email_blacklist: string[]; - url_scheme_whitelist: string[]; - - enforce_mime_types: boolean; - allowed_mime_types: string[]; - }; - - media: { - backend: string; - deduplicate_media: boolean; - conversion: { - convert_images: boolean; - convert_to: string; - }; - }; - - s3: { - endpoint: string; - access_key: string; - secret_access_key: string; - region: string; - bucket_name: string; - public_url: string; - }; - - defaults: { - visibility: string; - language: string; - avatar: string; - header: string; - }; - - email: { - send_on_report: boolean; - send_on_suspend: boolean; - send_on_unsuspend: boolean; - }; - - activitypub: { - use_tombstones: boolean; - reject_activities: string[]; - force_followers_only: string[]; - discard_reports: string[]; - discard_deletes: string[]; - discard_banners: string[]; - discard_avatars: string[]; - discard_updates: string[]; - discard_follows: string[]; - force_sensitive: string[]; - remove_media: string[]; - fetch_all_collection_members: boolean; - authorized_fetch: boolean; - }; - - filters: { - note_filters: string[]; - username_filters: string[]; - displayname_filters: string[]; - bio_filters: string[]; - emoji_filters: string[]; - }; - - logging: { - log_requests: boolean; - log_requests_verbose: boolean; - log_filters: boolean; - }; - - ratelimits: { - duration_coeff: number; - max_coeff: number; - }; - - custom_ratelimits: Record< - string, - { - duration: number; - max: number; - } - >; - [key: string]: unknown; -} - -export const configDefaults: ConfigType = { - http: { - bind: "http://0.0.0.0", - bind_port: "8000", - base_url: "http://lysand.localhost:8000", - banned_ips: [], - }, - database: { - host: "localhost", - port: 5432, - username: "postgres", - password: "postgres", - database: "lysand", - }, - redis: { - queue: { - host: "localhost", - port: 6379, - password: "", - database: 0, - }, - cache: { - host: "localhost", - port: 6379, - password: "", - database: 1, - enabled: false, - }, - }, - meilisearch: { - host: "localhost", - port: 1491, - api_key: "", - enabled: false, - }, - signups: { - tos_url: "", - rules: [], - registration: false, - }, - oidc: { - providers: [], - }, - instance: { - banner: "", - description: "", - logo: "", - name: "", - }, - smtp: { - password: "", - port: 465, - server: "", - tls: true, - username: "", - }, - media: { - backend: "local", - deduplicate_media: true, - conversion: { - convert_images: false, - convert_to: "webp", - }, - }, - email: { - send_on_report: false, - send_on_suspend: false, - send_on_unsuspend: false, - }, - s3: { - access_key: "", - bucket_name: "", - endpoint: "", - public_url: "", - region: "", - secret_access_key: "", - }, - validation: { - max_displayname_size: 50, - max_bio_size: 6000, - max_note_size: 5000, - max_avatar_size: 5_000_000, - max_header_size: 5_000_000, - max_media_size: 40_000_000, - max_media_attachments: 10, - max_media_description_size: 1000, - max_poll_options: 20, - max_poll_option_size: 500, - min_poll_duration: 60, - max_poll_duration: 1893456000, - max_username_size: 30, - - username_blacklist: [ - ".well-known", - "~", - "about", - "activities", - "api", - "auth", - "dev", - "inbox", - "internal", - "main", - "media", - "nodeinfo", - "notice", - "oauth", - "objects", - "proxy", - "push", - "registration", - "relay", - "settings", - "status", - "tag", - "users", - "web", - "search", - "mfa", - ], - - blacklist_tempmail: false, - - email_blacklist: [], - - url_scheme_whitelist: [ - "http", - "https", - "ftp", - "dat", - "dweb", - "gopher", - "hyper", - "ipfs", - "ipns", - "irc", - "xmpp", - "ircs", - "magnet", - "mailto", - "mumble", - "ssb", - ], - - enforce_mime_types: false, - allowed_mime_types: [], - }, - defaults: { - visibility: "public", - language: "en", - avatar: "", - header: "", - }, - activitypub: { - use_tombstones: true, - reject_activities: [], - force_followers_only: [], - discard_reports: [], - discard_deletes: [], - discard_banners: [], - discard_avatars: [], - force_sensitive: [], - discard_updates: [], - discard_follows: [], - remove_media: [], - fetch_all_collection_members: false, - authorized_fetch: false, - }, - filters: { - note_filters: [], - username_filters: [], - displayname_filters: [], - bio_filters: [], - emoji_filters: [], - }, - logging: { - log_requests: false, - log_requests_verbose: false, - log_filters: true, - }, - ratelimits: { - duration_coeff: 1, - max_coeff: 1, - }, - custom_ratelimits: {}, -}; - -export const getConfig = () => { - return { - ...configDefaults, - ...config, - }; -}; - -export const getHost = () => { - const url = new URL(getConfig().http.base_url); - - return url.host; -}; - -// Refresh config every 5 seconds -setInterval(() => { - scanConfig() - .then(newConfig => { - if (newConfig !== config) { - config = newConfig; - } - }) - .catch(e => { - console.error(e); - }); -}, 5000); - -export { config }; diff --git a/utils/constants.ts b/utils/constants.ts index 3e9c7686..4de00425 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,6 +1,6 @@ -import { getConfig } from "~classes/configmanager"; +import { ConfigManager } from "config-manager"; -const config = getConfig(); +const config = await new ConfigManager({}).getConfig(); export const oauthRedirectUri = (issuer: string) => `${config.http.base_url}/oauth/callback/${issuer}`;