Slight refactoring, begin work on major moderation overhaul

This commit is contained in:
Jesse Wierzbinski 2024-03-03 15:27:08 -10:00
parent e05dca9fc1
commit 2bc9ff51ea
No known key found for this signature in database
45 changed files with 639 additions and 109 deletions

View file

@ -2,7 +2,7 @@
* Usage: TOKEN=your_token_here bun benchmark:timeline <request_count>
*/
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import chalk from "chalk";
const config = getConfig();

443
classes/configmanager.ts Normal file
View file

@ -0,0 +1,443 @@
/**
* @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
* Fuses both and provides a way to retrieve individual values
*/
import { parse, stringify } from "@iarna/toml";
import { deepMerge, deepMergeArray } from "@merge";
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;
};
// 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[];
};
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 = () => {
// Deeply merge configDefaults, config and internalConfig
return deepMergeArray([
configDefaults,
config,
internalConfig,
]) as any as ConfigType;
};
/**
* Sets the internal config
* @param newConfig Any part of ConfigType
*/
export const setConfig = async (newConfig: Partial<ConfigType>) => {
const newInternalConfig = deepMerge(internalConfig, newConfig);
// 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
)}`
);
};
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 };

View file

@ -4,7 +4,7 @@ import {
PutObjectCommand,
S3Client,
} from "@aws-sdk/client-s3";
import type { ConfigType } from "@config";
import type { ConfigType } from "~classes/configmanager";
import sharp from "sharp";
import { exists, mkdir } from "fs/promises";
class MediaBackend {

2
cli.ts
View file

@ -4,7 +4,7 @@ import { client } from "~database/datasource";
import { createNewLocalUser } from "~database/entities/User";
import Table from "cli-table";
import { rebuildSearchIndexes, MeiliIndexType } from "@meilisearch";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { uploadFile } from "~classes/media";
import { getUrl } from "~database/entities/Attachment";
import { mkdir, exists } from "fs/promises";

View file

@ -1,4 +1,4 @@
import type { ConfigType } from "@config";
import type { ConfigType } from "~classes/configmanager";
import type { Attachment } from "@prisma/client";
import type { APIAsyncAttachment } from "~types/entities/async_attachment";
import type { APIAttachment } from "~types/entities/attachment";

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import type { Like as LysandLike } from "~types/lysand/Object";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import type { Like } from "@prisma/client";
import { client } from "~database/datasource";
import type { UserWithRelations } from "./User";

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { Worker } from "bullmq";
import { client, federationQueue } from "~database/datasource";
import {

View file

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import type { UserWithRelations } from "./User";
import {
fetchRemoteUser,

View file

@ -1,5 +1,5 @@
import type { ConfigType } from "@config";
import { getConfig } from "@config";
import type { ConfigType } from "~classes/configmanager";
import { getConfig } from "~classes/configmanager";
import type { APIAccount } from "~types/entities/account";
import type { User as LysandUser } from "~types/lysand/Object";
import { htmlToText } from "html-to-text";

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { jsonResponse } from "@response";
import chalk from "chalk";
import { appendFile } from "fs/promises";

View file

@ -1,9 +1,12 @@
// Proxies all `bunx prisma` commands with an environment variable
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
const config = getConfig();
process.stdout.write(
`postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}`
`postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}\n`
);
// Ends
process.exit(0);

View file

@ -0,0 +1,40 @@
-- AlterTable
ALTER TABLE "Instance" ADD COLUMN "disableAutomoderation" BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE "User" ADD COLUMN "disableAutomoderation" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "ModerationData" (
"id" UUID NOT NULL DEFAULT uuid_generate_v7(),
"statusId" UUID NOT NULL,
"creatorId" UUID NOT NULL,
"note" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ModerationData_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Flag" (
"id" UUID NOT NULL DEFAULT uuid_generate_v7(),
"statusId" UUID NOT NULL,
"userId" UUID NOT NULL,
"flagType" TEXT NOT NULL DEFAULT 'other',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Flag_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "ModerationData" ADD CONSTRAINT "ModerationData_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "Status"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ModerationData" ADD CONSTRAINT "ModerationData_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Flag" ADD CONSTRAINT "Flag_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "Status"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Flag" ADD CONSTRAINT "Flag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -37,14 +37,15 @@ model Emoji {
}
model Instance {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
base_url String
name String
version String
logo Json
emojis Emoji[] // One to many relation with Emoji
statuses Status[] // One to many relation with Status
users User[] // One to many relation with User
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
base_url String
name String
version String
logo Json
emojis Emoji[] // One to many relation with Emoji
statuses Status[] // One to many relation with Status
users User[] // One to many relation with User
disableAutomoderation Boolean @default(false)
}
model Like {
@ -93,38 +94,62 @@ model Relationship {
}
model Status {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique
author User @relation("UserStatuses", fields: [authorId], references: [id], onDelete: Cascade)
authorId String @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reblog Status? @relation("StatusToStatus", fields: [reblogId], references: [id], onDelete: Cascade)
reblogId String? @db.Uuid
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique
author User @relation("UserStatuses", fields: [authorId], references: [id], onDelete: Cascade)
authorId String @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reblog Status? @relation("StatusToStatus", fields: [reblogId], references: [id], onDelete: Cascade)
reblogId String? @db.Uuid
isReblog Boolean
content String @default("")
contentType String @default("text/plain")
contentSource String @default("")
content String @default("")
contentType String @default("text/plain")
contentSource String @default("")
visibility String
inReplyToPost Status? @relation("StatusToStatusReply", fields: [inReplyToPostId], references: [id], onDelete: SetNull)
inReplyToPostId String? @db.Uuid
quotingPost Status? @relation("StatusToStatusQuote", fields: [quotingPostId], references: [id], onDelete: SetNull)
quotingPostId String? @db.Uuid
instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String? @db.Uuid
inReplyToPost Status? @relation("StatusToStatusReply", fields: [inReplyToPostId], references: [id], onDelete: SetNull)
inReplyToPostId String? @db.Uuid
quotingPost Status? @relation("StatusToStatusQuote", fields: [quotingPostId], references: [id], onDelete: SetNull)
quotingPostId String? @db.Uuid
instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String? @db.Uuid
sensitive Boolean
spoilerText String @default("")
application Application? @relation(fields: [applicationId], references: [id], onDelete: SetNull)
applicationId String? @db.Uuid
emojis Emoji[] @relation
spoilerText String @default("")
application Application? @relation(fields: [applicationId], references: [id], onDelete: SetNull)
applicationId String? @db.Uuid
emojis Emoji[] @relation
mentions User[]
likes Like[] @relation("LikedToStatus")
reblogs Status[] @relation("StatusToStatus")
replies Status[] @relation("StatusToStatusReply")
quotes Status[] @relation("StatusToStatusQuote")
pinnedBy User[] @relation("UserPinnedNotes")
likes Like[] @relation("LikedToStatus")
reblogs Status[] @relation("StatusToStatus")
replies Status[] @relation("StatusToStatusReply")
quotes Status[] @relation("StatusToStatusQuote")
pinnedBy User[] @relation("UserPinnedNotes")
attachments Attachment[]
relatedNotifications Notification[]
flags Flag[]
moderationData ModerationData[]
}
model ModerationData {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
status Status @relation(fields: [statusId], references: [id], onDelete: Cascade)
statusId String @db.Uuid
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
creatorId String @db.Uuid
note String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Used for moderation purposes
model Flag {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
status Status @relation(fields: [statusId], references: [id], onDelete: Cascade)
statusId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String @db.Uuid
flagType String @default("other")
createdAt DateTime @default(now())
}
model Token {
@ -179,39 +204,42 @@ model Notification {
}
model User {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique
username String @unique
displayName String
password String? // Nullable
email String? @unique // Nullable
note String @default("")
isAdmin Boolean @default(false)
endpoints Json? // Nullable
source Json
avatar String
header String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isBot Boolean @default(false)
isLocked Boolean @default(false)
isDiscoverable Boolean @default(false)
sanctions String[] @default([])
publicKey String
privateKey String? // Nullable
relationships Relationship[] @relation("OwnerToRelationship") // One to many relation with Relationship
relationshipSubjects Relationship[] @relation("SubjectToRelationship") // One to many relation with Relationship
instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade) // Many to one relation with Instance
instanceId String? @db.Uuid
pinnedNotes Status[] @relation("UserPinnedNotes") // Many to many relation with Status
emojis Emoji[] // Many to many relation with Emoji
statuses Status[] @relation("UserStatuses") // One to many relation with Status
tokens Token[] // One to many relation with Token
likes Like[] @relation("UserLiked") // One to many relation with Like
statusesMentioned Status[] // Many to many relation with Status
notifications Notification[] // One to many relation with Notification
notified Notification[] @relation("NotificationToNotified") // One to many relation with Notification
linkedOpenIdAccounts OpenIdAccount[] // One to many relation with OpenIdAccount
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique
username String @unique
displayName String
password String? // Nullable
email String? @unique // Nullable
note String @default("")
isAdmin Boolean @default(false)
endpoints Json? // Nullable
source Json
avatar String
header String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isBot Boolean @default(false)
isLocked Boolean @default(false)
isDiscoverable Boolean @default(false)
sanctions String[] @default([])
publicKey String
privateKey String? // Nullable
relationships Relationship[] @relation("OwnerToRelationship") // One to many relation with Relationship
relationshipSubjects Relationship[] @relation("SubjectToRelationship") // One to many relation with Relationship
instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade) // Many to one relation with Instance
instanceId String? @db.Uuid
pinnedNotes Status[] @relation("UserPinnedNotes") // Many to many relation with Status
emojis Emoji[] // Many to many relation with Emoji
statuses Status[] @relation("UserStatuses") // One to many relation with Status
tokens Token[] // One to many relation with Token
likes Like[] @relation("UserLiked") // One to many relation with Like
statusesMentioned Status[] // Many to many relation with Status
notifications Notification[] // One to many relation with Notification
notified Notification[] @relation("NotificationToNotified") // One to many relation with Notification
linkedOpenIdAccounts OpenIdAccount[] // One to many relation with OpenIdAccount
flags Flag[] @relation
moderationData ModerationData[]
disableAutomoderation Boolean @default(false)
}
model OpenIdAccount {

View file

@ -1,5 +1,5 @@
import { MatchedRoute } from "bun";
import { getConfig, getHost } from "@config";
import { getConfig, getHost } from "~classes/configmanager";
import { xmlResponse } from "@response";
import { applyConfig } from "@api";

View file

@ -1,6 +1,6 @@
import { jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { applyConfig } from "@api";
export const meta = applyConfig({

View file

@ -1,5 +1,5 @@
import { MatchedRoute } from "bun";
import { getConfig, getHost } from "@config";
import { getConfig, getHost } from "~classes/configmanager";
import { applyConfig } from "@api";
export const meta = applyConfig({

View file

@ -1,6 +1,6 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { getConfig, getHost } from "@config";
import { getConfig, getHost } from "~classes/configmanager";
import { applyConfig } from "@api";
import { client } from "~database/datasource";

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { parseRequest } from "@request";
import { jsonResponse } from "@response";
import { tempmailDomains } from "@tempmail";

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import {

View file

@ -1,5 +1,5 @@
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { jsonResponse } from "@response";
import { client } from "~database/datasource";
import { userRelations, userToAPI } from "~database/entities/User";

View file

@ -4,7 +4,7 @@ import { client } from "~database/datasource";
import { getFromRequest } from "~database/entities/User";
import type { APIRouteMeta } from "~types/api";
import { uploadFile } from "~classes/media";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
import type { MatchedRoute } from "bun";
import { parseRequest } from "@request";

View file

@ -6,7 +6,7 @@ import { getFromRequest } from "~database/entities/User";
import type { APIRouteMeta } from "~types/api";
import sharp from "sharp";
import { uploadFile } from "~classes/media";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
export const meta: APIRouteMeta = applyConfig({

View file

@ -1,5 +1,5 @@
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml } from "@sanitization";

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import type { MatchedRoute } from "bun";

View file

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml } from "@sanitization";

View file

@ -6,7 +6,7 @@ import { getFromRequest } from "~database/entities/User";
import type { APIRouteMeta } from "~types/api";
import sharp from "sharp";
import { uploadFile } from "~classes/media";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
export const meta: APIRouteMeta = applyConfig({

View file

@ -1,5 +1,5 @@
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { MeiliIndexType, meilisearch } from "@meilisearch";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";

View file

@ -1,5 +1,5 @@
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { oauthRedirectUri } from "@constants";
import type { MatchedRoute } from "bun";
import {

View file

@ -1,5 +1,5 @@
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { oauthRedirectUri } from "@constants";
import type { MatchedRoute } from "bun";
import { randomBytes } from "crypto";

View file

@ -1,5 +1,5 @@
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { jsonResponse } from "@response";
export const meta = applyConfig({

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { getBestContentType } from "@content_types";
import { errorResponse, jsonResponse } from "@response";
import type { MatchedRoute } from "bun";

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { errorResponse, jsonResponse } from "@response";
import type { MatchedRoute } from "bun";
import { client } from "~database/datasource";

View file

@ -1,6 +1,6 @@
import { jsonResponse } from "@response";
import type { MatchedRoute } from "bun";
import { getConfig, getHost } from "@config";
import { getConfig, getHost } from "~classes/configmanager";
import { applyConfig } from "@api";
import {
statusAndUserRelations,

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import type { Token } from "@prisma/client";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { client } from "~database/datasource";

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import type { Token } from "@prisma/client";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { client } from "~database/datasource";

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import type { Token } from "@prisma/client";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { client } from "~database/datasource";

View file

@ -1,4 +1,4 @@
import { type ConfigType, getConfig } from "@config";
import { type ConfigType, getConfig } from "~classes/configmanager";
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { LocalBackend, S3Backend } from "~classes/media";
import { unlink } from "fs/promises";

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import type { Application, Token } from "@prisma/client";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { client } from "~database/datasource";

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import type { APIRouteMeta } from "~types/api";
export const applyConfig = (routeMeta: APIRouteMeta) => {

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
const config = getConfig();

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import chalk from "chalk";
import { client } from "~database/datasource";
import { Meilisearch } from "meilisearch";

16
utils/merge.ts Normal file
View file

@ -0,0 +1,16 @@
export const deepMerge = (
target: Record<string, any>,
source: Record<string, any>
) => {
const result = { ...target, ...source };
for (const key of Object.keys(result)) {
result[key] =
typeof target[key] == "object" && typeof source[key] == "object"
? deepMerge(target[key], source[key])
: structuredClone(result[key]);
}
return result;
};
export const deepMergeArray = (array: Record<string, any>[]) =>
array.reduce((ci, ni) => deepMerge(ci, ni), {});

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import type { Prisma } from "@prisma/client";
import chalk from "chalk";
import Redis from "ioredis";

View file

@ -1,4 +1,4 @@
import { getConfig } from "@config";
import { getConfig } from "~classes/configmanager";
import { sanitize } from "isomorphic-dompurify";
export const sanitizeHtml = async (html: string) => {