Replace eslint and prettier with Biome

This commit is contained in:
Jesse Wierzbinski 2024-04-06 19:30:49 -10:00
parent 4a5a2ea590
commit af0d627f19
No known key found for this signature in database
199 changed files with 16493 additions and 16361 deletions

View file

@ -17,6 +17,6 @@ module.exports = {
"@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/consistent-type-exports": "error", "@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/consistent-type-imports": "error" "@typescript-eslint/consistent-type-imports": "error",
}, },
}; };

5
.vscode/launch.json vendored
View file

@ -4,10 +4,7 @@
"type": "node", "type": "node",
"name": "vscode-jest-tests.v2.lysand", "name": "vscode-jest-tests.v2.lysand",
"request": "launch", "request": "launch",
"args": [ "args": ["test", "${jest.testFile}"],
"test",
"${jest.testFile}"
],
"cwd": "/home/jessew/Dev/lysand", "cwd": "/home/jessew/Dev/lysand",
"console": "integratedTerminal", "console": "integratedTerminal",
"internalConsoleOptions": "neverOpen", "internalConsoleOptions": "neverOpen",

View file

@ -5,9 +5,9 @@ const requests: Promise<Response>[] = [];
// Repeat 1000 times // Repeat 1000 times
for (let i = 0; i < 1000; i++) { for (let i = 0; i < 1000; i++) {
requests.push( requests.push(
fetch(`https://mastodon.social`, { fetch("https://mastodon.social", {
method: "GET", method: "GET",
}) }),
); );
} }

View file

@ -11,8 +11,8 @@ const requestCount = Number(process.argv[2]) || 100;
if (!token) { if (!token) {
console.log( console.log(
`${chalk.red( `${chalk.red(
"✗" "✗",
)} No token provided. Provide one via the TOKEN environment variable.` )} No token provided. Provide one via the TOKEN environment variable.`,
); );
process.exit(1); process.exit(1);
} }
@ -22,33 +22,33 @@ const fetchTimeline = () =>
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
}).then(res => res.ok); }).then((res) => res.ok);
const timeNow = performance.now(); const timeNow = performance.now();
const requests = Array.from({ length: requestCount }, () => fetchTimeline()); const requests = Array.from({ length: requestCount }, () => fetchTimeline());
Promise.all(requests) Promise.all(requests)
.then(results => { .then((results) => {
const timeTaken = performance.now() - timeNow; const timeTaken = performance.now() - timeNow;
if (results.every(t => t)) { if (results.every((t) => t)) {
console.log(`${chalk.green("✓")} All requests succeeded`); console.log(`${chalk.green("✓")} All requests succeeded`);
} else { } else {
console.log( console.log(
`${chalk.red("✗")} ${ `${chalk.red("✗")} ${
results.filter(t => !t).length results.filter((t) => !t).length
} requests failed` } requests failed`,
); );
} }
console.log( console.log(
`${chalk.green("✓")} ${ `${chalk.green("✓")} ${
requests.length requests.length
} requests fulfilled in ${chalk.bold( } requests fulfilled in ${chalk.bold(
(timeTaken / 1000).toFixed(5) (timeTaken / 1000).toFixed(5),
)}s` )}s`,
); );
}) })
.catch(err => { .catch((err) => {
console.log(`${chalk.red("✗")} ${err}`); console.log(`${chalk.red("✗")} ${err}`);
process.exit(1); process.exit(1);
}); });

View file

@ -1,17 +1,20 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": { "organizeImports": {
"enabled": true "enabled": true,
"ignore": ["node_modules/**/*", "dist/**/*"]
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true "recommended": true
} },
"ignore": ["node_modules/**/*", "dist/**/*"]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 4 "indentWidth": 4,
"ignore": ["node_modules/**/*", "dist/**/*"]
} }
} }

View file

@ -1,5 +1,5 @@
// Delete dist directory // Delete dist directory
import { rm, cp, mkdir, exists } from "fs/promises"; import { cp, exists, mkdir, rm } from "node:fs/promises";
import { rawRoutes } from "~routes"; import { rawRoutes } from "~routes";
if (!(await exists("./pages/dist"))) { if (!(await exists("./pages/dist"))) {
@ -11,23 +11,23 @@ console.log(`Building at ${process.cwd()}`);
await rm("./dist", { recursive: true }); await rm("./dist", { recursive: true });
await mkdir(process.cwd() + "/dist"); await mkdir(`${process.cwd()}/dist`);
//bun build --entrypoints ./index.ts ./prisma.ts ./cli.ts --outdir dist --target bun --splitting --minify --external bullmq,@prisma/client //bun build --entrypoints ./index.ts ./prisma.ts ./cli.ts --outdir dist --target bun --splitting --minify --external bullmq,@prisma/client
await Bun.build({ await Bun.build({
entrypoints: [ entrypoints: [
process.cwd() + "/index.ts", `${process.cwd()}/index.ts`,
process.cwd() + "/prisma.ts", `${process.cwd()}/prisma.ts`,
process.cwd() + "/cli.ts", `${process.cwd()}/cli.ts`,
// Force Bun to include endpoints // Force Bun to include endpoints
...Object.values(rawRoutes), ...Object.values(rawRoutes),
], ],
outdir: process.cwd() + "/dist", outdir: `${process.cwd()}/dist`,
target: "bun", target: "bun",
splitting: true, splitting: true,
minify: true, minify: true,
external: ["bullmq"], external: ["bullmq"],
}).then(output => { }).then((output) => {
if (!output.success) { if (!output.success) {
console.log(output.logs); console.log(output.logs);
} }
@ -35,18 +35,18 @@ await Bun.build({
// Create pages directory // Create pages directory
// mkdir ./dist/pages // mkdir ./dist/pages
await mkdir(process.cwd() + "/dist/pages"); await mkdir(`${process.cwd()}/dist/pages`);
// Copy Vite build output to dist // Copy Vite build output to dist
// cp -r ./pages/dist ./dist/pages // cp -r ./pages/dist ./dist/pages
await cp(process.cwd() + "/pages/dist", process.cwd() + "/dist/pages/", { await cp(`${process.cwd()}/pages/dist`, `${process.cwd()}/dist/pages/`, {
recursive: true, recursive: true,
}); });
// Copy the Bee Movie script from pages // Copy the Bee Movie script from pages
await cp( await cp(
process.cwd() + "/pages/beemovie.txt", `${process.cwd()}/pages/beemovie.txt`,
process.cwd() + "/dist/pages/beemovie.txt" `${process.cwd()}/dist/pages/beemovie.txt`,
); );
console.log(`Built!`); console.log("Built!");

BIN
bun.lockb

Binary file not shown.

View file

@ -1,64 +0,0 @@
import type { APActivity, APActor } from "activitypub-types";
export class RemoteActor {
private actorData: APActor | null;
private actorUri: string;
constructor(actor: APActor | string) {
if (typeof actor === "string") {
this.actorUri = actor;
this.actorData = null;
} else {
this.actorUri = actor.id || "";
this.actorData = actor;
}
}
public async fetch() {
const response = await fetch(this.actorUri);
const actorJson = (await response.json()) as APActor;
this.actorData = actorJson;
}
public getData() {
return this.actorData;
}
}
export class RemoteActivity {
private data: APActivity | null;
private uri: string;
constructor(uri: string, data: APActivity | null) {
this.uri = uri;
this.data = data;
}
public async fetch() {
const response = await fetch(this.uri);
const json = (await response.json()) as APActivity;
this.data = json;
}
public getData() {
return this.data;
}
public async getActor() {
if (!this.data) {
throw new Error("No data");
}
if (Array.isArray(this.data.actor)) {
throw new Error("Multiple actors");
}
if (typeof this.data.actor === "string") {
const actor = new RemoteActor(this.data.actor);
await actor.fetch();
return actor.getData();
}
return new RemoteActor(this.data.actor as any);
}
}

428
cli.ts

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
import type { APIApplication } from "~types/entities/application";
import type { Application } from "@prisma/client"; import type { Application } from "@prisma/client";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import type { APIApplication } from "~types/entities/application";
/** /**
* Represents an application that can authenticate with the API. * Represents an application that can authenticate with the API.
@ -12,7 +12,7 @@ import { client } from "~database/datasource";
* @returns The application associated with the given access token, or null if no such application exists. * @returns The application associated with the given access token, or null if no such application exists.
*/ */
export const getFromToken = async ( export const getFromToken = async (
token: string token: string,
): Promise<Application | null> => { ): Promise<Application | null> => {
const dbToken = await client.token.findFirst({ const dbToken = await client.token.findFirst({
where: { where: {

View file

@ -1,11 +1,11 @@
import type { Attachment } from "@prisma/client"; import type { Attachment } from "@prisma/client";
import type { ConfigType } from "config-manager"; import type { Config } from "config-manager";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import type { APIAsyncAttachment } from "~types/entities/async_attachment"; import type { APIAsyncAttachment } from "~types/entities/async_attachment";
import type { APIAttachment } from "~types/entities/attachment"; import type { APIAttachment } from "~types/entities/attachment";
export const attachmentToAPI = ( export const attachmentToAPI = (
attachment: Attachment attachment: Attachment,
): APIAsyncAttachment | APIAttachment => { ): APIAsyncAttachment | APIAttachment => {
let type = "unknown"; let type = "unknown";
@ -19,7 +19,7 @@ export const attachmentToAPI = (
return { return {
id: attachment.id, id: attachment.id,
type: type as any, type: type as "image" | "video" | "audio" | "unknown",
url: attachment.url, url: attachment.url,
remote_url: attachment.remote_url, remote_url: attachment.remote_url,
preview_url: attachment.thumbnail_url, preview_url: attachment.thumbnail_url,
@ -57,12 +57,13 @@ export const attachmentToAPI = (
}; };
}; };
export const getUrl = (name: string, config: ConfigType) => { export const getUrl = (name: string, config: Config) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (config.media.backend === MediaBackendType.LOCAL) { if (config.media.backend === MediaBackendType.LOCAL) {
return `${config.http.base_url}/media/${name}`; return `${config.http.base_url}/media/${name}`;
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison, @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison, @typescript-eslint/no-unnecessary-condition
} else if (config.media.backend === MediaBackendType.S3) { }
if (config.media.backend === MediaBackendType.S3) {
return `${config.s3.public_url}/${name}`; return `${config.s3.public_url}/${name}`;
} }
return ""; return "";

View file

@ -1,7 +1,7 @@
import type { Emoji } from "@prisma/client";
import { client } from "~database/datasource";
import type { APIEmoji } from "~types/entities/emoji"; import type { APIEmoji } from "~types/entities/emoji";
import type { Emoji as LysandEmoji } from "~types/lysand/extensions/org.lysand/custom_emojis"; import type { Emoji as LysandEmoji } from "~types/lysand/extensions/org.lysand/custom_emojis";
import { client } from "~database/datasource";
import type { Emoji } from "@prisma/client";
/** /**
* Represents an emoji entity in the database. * Represents an emoji entity in the database.
@ -19,7 +19,7 @@ export const parseEmojis = async (text: string): Promise<Emoji[]> => {
return await client.emoji.findMany({ return await client.emoji.findMany({
where: { where: {
shortcode: { shortcode: {
in: matches.map(match => match.replace(/:/g, "")), in: matches.map((match) => match.replace(/:/g, "")),
}, },
instanceId: null, instanceId: null,
}, },
@ -81,7 +81,7 @@ export const emojiToLysand = (emoji: Emoji): LysandEmoji => {
* Converts the emoji to an ActivityPub object. * Converts the emoji to an ActivityPub object.
* @returns The ActivityPub object. * @returns The ActivityPub object.
*/ */
export const emojiToActivityPub = (emoji: Emoji): any => { export const emojiToActivityPub = (emoji: Emoji): object => {
// replace any with your ActivityPub Emoji type // replace any with your ActivityPub Emoji type
return { return {
type: "Emoji", type: "Emoji",

View file

@ -12,7 +12,7 @@ import type { ServerMetadata } from "~types/lysand/Object";
* @returns Either the database instance if it already exists, or a newly created instance. * @returns Either the database instance if it already exists, or a newly created instance.
*/ */
export const addInstanceIfNotExists = async ( export const addInstanceIfNotExists = async (
url: string url: string,
): Promise<Instance> => { ): Promise<Instance> => {
const origin = new URL(url).origin; const origin = new URL(url).origin;
const hostname = new URL(url).hostname; const hostname = new URL(url).hostname;
@ -26,8 +26,8 @@ export const addInstanceIfNotExists = async (
if (found) return found; if (found) return found;
// Fetch the instance configuration // Fetch the instance configuration
const metadata = (await fetch(`${origin}/.well-known/lysand`).then(res => const metadata = (await fetch(`${origin}/.well-known/lysand`).then((res) =>
res.json() res.json(),
)) as Partial<ServerMetadata>; )) as Partial<ServerMetadata>;
if (metadata.type !== "ServerMetadata") { if (metadata.type !== "ServerMetadata") {
@ -43,7 +43,7 @@ export const addInstanceIfNotExists = async (
base_url: hostname, base_url: hostname,
name: metadata.name, name: metadata.name,
version: metadata.version, version: metadata.version,
logo: metadata.logo as any, logo: metadata.logo,
}, },
}); });
}; };

View file

@ -1,10 +1,10 @@
import type { Like, Prisma } from "@prisma/client";
import { config } from "config-manager";
import { client } from "~database/datasource";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import type { Like as LysandLike } from "~types/lysand/Object"; import type { Like as LysandLike } from "~types/lysand/Object";
import type { Like } from "@prisma/client";
import { client } from "~database/datasource";
import type { UserWithRelations } from "./User";
import type { StatusWithRelations } from "./Status"; import type { StatusWithRelations } from "./Status";
import { config } from "config-manager"; import type { UserWithRelations } from "./User";
/** /**
* Represents a Like entity in the database. * Represents a Like entity in the database.
@ -12,9 +12,11 @@ import { config } from "config-manager";
export const toLysand = (like: Like): LysandLike => { export const toLysand = (like: Like): LysandLike => {
return { return {
id: like.id, id: like.id,
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
author: (like as any).liker?.uri, author: (like as any).liker?.uri,
type: "Like", type: "Like",
created_at: new Date(like.createdAt).toISOString(), created_at: new Date(like.createdAt).toISOString(),
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
object: (like as any).liked?.uri, object: (like as any).liked?.uri,
uri: `${config.http.base_url}/actions/${like.id}`, uri: `${config.http.base_url}/actions/${like.id}`,
}; };
@ -27,7 +29,7 @@ export const toLysand = (like: Like): LysandLike => {
*/ */
export const createLike = async ( export const createLike = async (
user: UserWithRelations, user: UserWithRelations,
status: StatusWithRelations status: StatusWithRelations,
) => { ) => {
await client.like.create({ await client.like.create({
data: { data: {
@ -58,7 +60,7 @@ export const createLike = async (
*/ */
export const deleteLike = async ( export const deleteLike = async (
user: UserWithRelations, user: UserWithRelations,
status: StatusWithRelations status: StatusWithRelations,
) => { ) => {
await client.like.deleteMany({ await client.like.deleteMany({
where: { where: {

View file

@ -9,7 +9,7 @@ export type NotificationWithRelations = Notification & {
}; };
export const notificationToAPI = async ( export const notificationToAPI = async (
notification: NotificationWithRelations notification: NotificationWithRelations,
): Promise<APINotification> => { ): Promise<APINotification> => {
return { return {
account: userToAPI(notification.account), account: userToAPI(notification.account),

View file

@ -21,6 +21,7 @@ export const createFromObject = async (object: LysandObjectType) => {
} }
const author = await client.lysandObject.findFirst({ const author = await client.lysandObject.findFirst({
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
where: { uri: (object as any).author }, where: { uri: (object as any).author },
}); });
@ -43,8 +44,8 @@ export const createFromObject = async (object: LysandObjectType) => {
"extensions", "extensions",
"type", "type",
"uri", "uri",
].includes(key) ].includes(key),
) ),
), ),
}, },
}); });
@ -56,7 +57,6 @@ export const toLysand = (lyObject: LysandObject): LysandObjectType => {
created_at: new Date(lyObject.created_at).toISOString(), created_at: new Date(lyObject.created_at).toISOString(),
type: lyObject.type, type: lyObject.type,
uri: lyObject.uri, uri: lyObject.uri,
// @ts-expect-error This works, I promise
...lyObject.extra_data, ...lyObject.extra_data,
extensions: lyObject.extensions, extensions: lyObject.extensions,
}; };

View file

@ -1,7 +1,7 @@
// import { Worker } from "bullmq";
import { statusToLysand, type StatusWithRelations } from "./Status";
import type { User } from "@prisma/client"; import type { User } from "@prisma/client";
import { config } from "config-manager"; import { config } from "config-manager";
// import { Worker } from "bullmq";
import { type StatusWithRelations, statusToLysand } from "./Status";
/* export const federationWorker = new Worker( /* export const federationWorker = new Worker(
"federation", "federation",
@ -134,19 +134,19 @@ export const str2ab = (str: string) => {
export const federateStatusTo = async ( export const federateStatusTo = async (
status: StatusWithRelations, status: StatusWithRelations,
sender: User, sender: User,
user: User user: User,
) => { ) => {
const privateKey = await crypto.subtle.importKey( const privateKey = await crypto.subtle.importKey(
"pkcs8", "pkcs8",
str2ab(atob(user.privateKey ?? "")), str2ab(atob(user.privateKey ?? "")),
"Ed25519", "Ed25519",
false, false,
["sign"] ["sign"],
); );
const digest = await crypto.subtle.digest( const digest = await crypto.subtle.digest(
"SHA-256", "SHA-256",
new TextEncoder().encode("request_body") new TextEncoder().encode("request_body"),
); );
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
@ -162,13 +162,13 @@ export const federateStatusTo = async (
`host: ${userInbox.host}\n` + `host: ${userInbox.host}\n` +
`date: ${date.toUTCString()}\n` + `date: ${date.toUTCString()}\n` +
`digest: SHA-256=${btoa( `digest: SHA-256=${btoa(
String.fromCharCode(...new Uint8Array(digest)) String.fromCharCode(...new Uint8Array(digest)),
)}\n` )}\n`,
) ),
); );
const signatureBase64 = btoa( const signatureBase64 = btoa(
String.fromCharCode(...new Uint8Array(signature)) String.fromCharCode(...new Uint8Array(signature)),
); );
return fetch(userInbox, { return fetch(userInbox, {

View file

@ -1,6 +1,6 @@
import type { Relationship, User } from "@prisma/client"; import type { Relationship, User } from "@prisma/client";
import type { APIRelationship } from "~types/entities/relationship";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import type { APIRelationship } from "~types/entities/relationship";
/** /**
* Stores Mastodon API relationships * Stores Mastodon API relationships
@ -14,7 +14,7 @@ import { client } from "~database/datasource";
*/ */
export const createNewRelationship = async ( export const createNewRelationship = async (
owner: User, owner: User,
other: User other: User,
): Promise<Relationship> => { ): Promise<Relationship> => {
return await client.relationship.create({ return await client.relationship.create({
data: { data: {
@ -40,7 +40,7 @@ export const createNewRelationship = async (
export const checkForBidirectionalRelationships = async ( export const checkForBidirectionalRelationships = async (
user1: User, user1: User,
user2: User, user2: User,
createIfNotExists = true createIfNotExists = true,
): Promise<boolean> => { ): Promise<boolean> => {
const relationship1 = await client.relationship.findFirst({ const relationship1 = await client.relationship.findFirst({
where: { where: {

View file

@ -1,29 +1,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import type { UserWithRelations } from "./User";
import { fetchRemoteUser, parseMentionsUris, userToAPI } from "./User";
import { client } from "~database/datasource";
import type { LysandPublication, Note } from "~types/lysand/Object";
import { htmlToText } from "html-to-text";
import { getBestContentType } from "@content_types"; import { getBestContentType } from "@content_types";
import { addStausToMeilisearch } from "@meilisearch";
import { import {
Prisma,
type Application, type Application,
type Emoji, type Emoji,
Prisma,
type Relationship, type Relationship,
type Status, type Status,
type User, type User,
} from "@prisma/client"; } from "@prisma/client";
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji"; import { sanitizeHtml } from "@sanitization";
import { config } from "config-manager";
import { htmlToText } from "html-to-text";
import linkifyHtml from "linkify-html";
import linkifyStr from "linkify-string";
import { parse } from "marked";
import { client } from "~database/datasource";
import type { APIAttachment } from "~types/entities/attachment";
import type { APIStatus } from "~types/entities/status"; import type { APIStatus } from "~types/entities/status";
import type { LysandPublication, Note } from "~types/lysand/Object";
import { applicationToAPI } from "./Application"; import { applicationToAPI } from "./Application";
import { attachmentToAPI } from "./Attachment"; import { attachmentToAPI } from "./Attachment";
import type { APIAttachment } from "~types/entities/attachment"; import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
import { sanitizeHtml } from "@sanitization"; /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { parse } from "marked"; import type { UserWithRelations } from "./User";
import linkifyStr from "linkify-string"; import { fetchRemoteUser, parseMentionsUris, userToAPI } from "./User";
import linkifyHtml from "linkify-html";
import { addStausToMeilisearch } from "@meilisearch";
import { config } from "config-manager";
import { statusAndUserRelations, userRelations } from "./relations"; import { statusAndUserRelations, userRelations } from "./relations";
const statusRelations = Prisma.validator<Prisma.StatusDefaultArgs>()({ const statusRelations = Prisma.validator<Prisma.StatusDefaultArgs>()({
@ -46,16 +46,15 @@ export type StatusWithRelations = Prisma.StatusGetPayload<
export const isViewableByUser = (status: Status, user: User | null) => { export const isViewableByUser = (status: Status, user: User | null) => {
if (status.authorId === user?.id) return true; if (status.authorId === user?.id) return true;
if (status.visibility === "public") return true; if (status.visibility === "public") return true;
else if (status.visibility === "unlisted") return true; if (status.visibility === "unlisted") return true;
else if (status.visibility === "private") { if (status.visibility === "private") {
// @ts-expect-error Prisma TypeScript types dont include relations // @ts-expect-error Prisma TypeScript types dont include relations
return !!(user?.relationships as Relationship[]).find( return !!(user?.relationships as Relationship[]).find(
rel => rel.id === status.authorId (rel) => rel.id === status.authorId,
); );
} else { }
// @ts-expect-error Prisma TypeScript types dont include relations // @ts-expect-error Prisma TypeScript types dont include relations
return user && (status.mentions as User[]).includes(user); return user && (status.mentions as User[]).includes(user);
}
}; };
export const fetchFromRemote = async (uri: string): Promise<Status | null> => { export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
@ -109,7 +108,7 @@ export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
reply: replyStatus reply: replyStatus
? { ? {
status: replyStatus, status: replyStatus,
user: (replyStatus as any).author, user: (replyStatus as StatusWithRelations).author,
} }
: undefined, : undefined,
quote: quotingStatus || undefined, quote: quotingStatus || undefined,
@ -122,7 +121,7 @@ export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
export const getAncestors = async ( export const getAncestors = async (
status: StatusWithRelations, status: StatusWithRelations,
fetcher: UserWithRelations | null fetcher: UserWithRelations | null,
) => { ) => {
const ancestors: StatusWithRelations[] = []; const ancestors: StatusWithRelations[] = [];
@ -145,8 +144,8 @@ export const getAncestors = async (
// Filter for posts that are viewable by the user // Filter for posts that are viewable by the user
const viewableAncestors = ancestors.filter(ancestor => const viewableAncestors = ancestors.filter((ancestor) =>
isViewableByUser(ancestor, fetcher) isViewableByUser(ancestor, fetcher),
); );
return viewableAncestors; return viewableAncestors;
}; };
@ -159,7 +158,7 @@ export const getAncestors = async (
export const getDescendants = async ( export const getDescendants = async (
status: StatusWithRelations, status: StatusWithRelations,
fetcher: UserWithRelations | null, fetcher: UserWithRelations | null,
depth = 0 depth = 0,
) => { ) => {
const descendants: StatusWithRelations[] = []; const descendants: StatusWithRelations[] = [];
@ -181,7 +180,7 @@ export const getDescendants = async (
const childDescendants = await getDescendants( const childDescendants = await getDescendants(
child, child,
fetcher, fetcher,
depth + 1 depth + 1,
); );
descendants.push(...childDescendants); descendants.push(...childDescendants);
} }
@ -189,8 +188,8 @@ export const getDescendants = async (
// Filter for posts that are viewable by the user // Filter for posts that are viewable by the user
const viewableDescendants = descendants.filter(descendant => const viewableDescendants = descendants.filter((descendant) =>
isViewableByUser(descendant, fetcher) isViewableByUser(descendant, fetcher),
); );
return viewableDescendants; return viewableDescendants;
}; };
@ -233,7 +232,7 @@ export const createNewStatus = async (data: {
if (mentions.length === 0) { if (mentions.length === 0) {
mentions = await client.user.findMany({ mentions = await client.user.findMany({
where: { where: {
OR: mentionedPeople.map(person => ({ OR: mentionedPeople.map((person) => ({
username: person.split("@")[1], username: person.split("@")[1],
instance: { instance: {
base_url: person.split("@")[2], base_url: person.split("@")[2],
@ -244,12 +243,12 @@ export const createNewStatus = async (data: {
}); });
} }
let formattedContent; let formattedContent = "";
// Get HTML version of content // Get HTML version of content
if (data.content_type === "text/markdown") { if (data.content_type === "text/markdown") {
formattedContent = linkifyHtml( formattedContent = linkifyHtml(
await sanitizeHtml(await parse(data.content)) await sanitizeHtml(await parse(data.content)),
); );
} else if (data.content_type === "text/x.misskeymarkdown") { } else if (data.content_type === "text/x.misskeymarkdown") {
// Parse as MFM // Parse as MFM
@ -260,7 +259,7 @@ export const createNewStatus = async (data: {
// Split by newline and add <p> tags // Split by newline and add <p> tags
formattedContent = formattedContent formattedContent = formattedContent
.split("\n") .split("\n")
.map(line => `<p>${line}</p>`) .map((line) => `<p>${line}</p>`)
.join("\n"); .join("\n");
} }
@ -275,7 +274,7 @@ export const createNewStatus = async (data: {
sensitive: data.sensitive, sensitive: data.sensitive,
spoilerText: data.spoiler_text, spoilerText: data.spoiler_text,
emojis: { emojis: {
connect: data.emojis.map(emoji => { connect: data.emojis.map((emoji) => {
return { return {
id: emoji.id, id: emoji.id,
}; };
@ -283,7 +282,7 @@ export const createNewStatus = async (data: {
}, },
attachments: data.media_attachments attachments: data.media_attachments
? { ? {
connect: data.media_attachments.map(attachment => { connect: data.media_attachments.map((attachment) => {
return { return {
id: attachment, id: attachment,
}; };
@ -298,7 +297,7 @@ export const createNewStatus = async (data: {
data.uri || data.uri ||
`${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`, `${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`,
mentions: { mentions: {
connect: mentions.map(mention => { connect: mentions.map((mention) => {
return { return {
id: mention.id, id: mention.id,
}; };
@ -349,7 +348,7 @@ export const editStatus = async (
uri?: string; uri?: string;
mentions?: User[]; mentions?: User[];
media_attachments?: string[]; media_attachments?: string[];
} },
) => { ) => {
// Get people mentioned in the content (match @username or @username@domain.com mentions // Get people mentioned in the content (match @username or @username@domain.com mentions
const mentionedPeople = const mentionedPeople =
@ -366,7 +365,7 @@ export const editStatus = async (
if (mentions.length === 0) { if (mentions.length === 0) {
mentions = await client.user.findMany({ mentions = await client.user.findMany({
where: { where: {
OR: mentionedPeople.map(person => ({ OR: mentionedPeople.map((person) => ({
username: person.split("@")[1], username: person.split("@")[1],
instance: { instance: {
base_url: person.split("@")[2], base_url: person.split("@")[2],
@ -377,12 +376,12 @@ export const editStatus = async (
}); });
} }
let formattedContent; let formattedContent = "";
// Get HTML version of content // Get HTML version of content
if (data.content_type === "text/markdown") { if (data.content_type === "text/markdown") {
formattedContent = linkifyHtml( formattedContent = linkifyHtml(
await sanitizeHtml(await parse(data.content)) await sanitizeHtml(await parse(data.content)),
); );
} else if (data.content_type === "text/x.misskeymarkdown") { } else if (data.content_type === "text/x.misskeymarkdown") {
// Parse as MFM // Parse as MFM
@ -393,7 +392,7 @@ export const editStatus = async (
// Split by newline and add <p> tags // Split by newline and add <p> tags
formattedContent = formattedContent formattedContent = formattedContent
.split("\n") .split("\n")
.map(line => `<p>${line}</p>`) .map((line) => `<p>${line}</p>`)
.join("\n"); .join("\n");
} }
@ -409,7 +408,7 @@ export const editStatus = async (
sensitive: data.sensitive, sensitive: data.sensitive,
spoilerText: data.spoiler_text, spoilerText: data.spoiler_text,
emojis: { emojis: {
connect: data.emojis.map(emoji => { connect: data.emojis.map((emoji) => {
return { return {
id: emoji.id, id: emoji.id,
}; };
@ -417,7 +416,7 @@ export const editStatus = async (
}, },
attachments: data.media_attachments attachments: data.media_attachments
? { ? {
connect: data.media_attachments.map(attachment => { connect: data.media_attachments.map((attachment) => {
return { return {
id: attachment, id: attachment,
}; };
@ -425,7 +424,7 @@ export const editStatus = async (
} }
: undefined, : undefined,
mentions: { mentions: {
connect: mentions.map(mention => { connect: mentions.map((mention) => {
return { return {
id: mention.id, id: mention.id,
}; };
@ -453,7 +452,7 @@ export const isFavouritedBy = async (status: Status, user: User) => {
*/ */
export const statusToAPI = async ( export const statusToAPI = async (
status: StatusWithRelations, status: StatusWithRelations,
user?: UserWithRelations user?: UserWithRelations,
): Promise<APIStatus> => { ): Promise<APIStatus> => {
return { return {
id: status.id, id: status.id,
@ -467,25 +466,25 @@ export const statusToAPI = async (
: null, : null,
card: null, card: null,
content: status.content, content: status.content,
emojis: status.emojis.map(emoji => emojiToAPI(emoji)), emojis: status.emojis.map((emoji) => emojiToAPI(emoji)),
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
favourited: !!(status.likes ?? []).find( favourited: !!(status.likes ?? []).find(
like => like.likerId === user?.id (like) => like.likerId === user?.id,
), ),
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
favourites_count: (status.likes ?? []).length, favourites_count: (status.likes ?? []).length,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
media_attachments: (status.attachments ?? []).map( media_attachments: (status.attachments ?? []).map(
a => attachmentToAPI(a) as APIAttachment (a) => attachmentToAPI(a) as APIAttachment,
), ),
// @ts-expect-error Prisma TypeScript types dont include relations // @ts-expect-error Prisma TypeScript types dont include relations
mentions: status.mentions.map(mention => userToAPI(mention)), mentions: status.mentions.map((mention) => userToAPI(mention)),
language: null, language: null,
muted: user muted: user
? user.relationships.find(r => r.subjectId == status.authorId) ? user.relationships.find((r) => r.subjectId === status.authorId)
?.muting || false ?.muting || false
: false, : false,
pinned: status.pinnedBy.find(u => u.id === user?.id) ? true : false, pinned: status.pinnedBy.find((u) => u.id === user?.id) ? true : false,
// TODO: Add pols // TODO: Add pols
poll: null, poll: null,
reblog: status.reblog reblog: status.reblog
@ -508,7 +507,7 @@ export const statusToAPI = async (
bookmarked: false, bookmarked: false,
quote: status.quotingPost quote: status.quotingPost
? await statusToAPI( ? await statusToAPI(
status.quotingPost as unknown as StatusWithRelations status.quotingPost as unknown as StatusWithRelations,
) )
: null, : null,
quote_id: status.quotingPost?.id || undefined, quote_id: status.quotingPost?.id || undefined,
@ -583,13 +582,13 @@ export const statusToLysand = (status: StatusWithRelations): Note => {
// TODO: Add attachments // TODO: Add attachments
attachments: [], attachments: [],
is_sensitive: status.sensitive, is_sensitive: status.sensitive,
mentions: status.mentions.map(mention => mention.uri), mentions: status.mentions.map((mention) => mention.uri),
quotes: status.quotingPost ? [status.quotingPost.uri] : [], quotes: status.quotingPost ? [status.quotingPost.uri] : [],
replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [], replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [],
subject: status.spoilerText, subject: status.spoilerText,
extensions: { extensions: {
"org.lysand:custom_emojis": { "org.lysand:custom_emojis": {
emojis: status.emojis.map(emoji => emojiToLysand(emoji)), emojis: status.emojis.map((emoji) => emojiToLysand(emoji)),
}, },
// TODO: Add polls and reactions // TODO: Add polls and reactions
}, },

View file

@ -1,16 +1,16 @@
import type { APIAccount } from "~types/entities/account"; import { addUserToMeilisearch } from "@meilisearch";
import type { LysandUser } from "~types/lysand/Object";
import { htmlToText } from "html-to-text";
import type { User } from "@prisma/client"; import type { User } from "@prisma/client";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { type Config, config } from "config-manager";
import { htmlToText } from "html-to-text";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { MediaBackendType } from "~packages/media-manager";
import type { APIAccount } from "~types/entities/account";
import type { APISource } from "~types/entities/source";
import type { LysandUser } from "~types/lysand/Object";
import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji"; import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji";
import { addInstanceIfNotExists } from "./Instance"; import { addInstanceIfNotExists } from "./Instance";
import type { APISource } from "~types/entities/source";
import { addUserToMeilisearch } from "@meilisearch";
import { config, type Config } from "config-manager";
import { userRelations } from "./relations"; import { userRelations } from "./relations";
import { MediaBackendType } from "~packages/media-manager";
export interface AuthData { export interface AuthData {
user: UserWithRelations | null; user: UserWithRelations | null;
@ -38,7 +38,8 @@ export const getAvatarUrl = (user: User, config: Config) => {
if (config.media.backend === MediaBackendType.LOCAL) { if (config.media.backend === MediaBackendType.LOCAL) {
return `${config.http.base_url}/media/${user.avatar}`; return `${config.http.base_url}/media/${user.avatar}`;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (config.media.backend === MediaBackendType.S3) { }
if (config.media.backend === MediaBackendType.S3) {
return `${config.s3.public_url}/${user.avatar}`; return `${config.s3.public_url}/${user.avatar}`;
} }
return ""; return "";
@ -54,7 +55,8 @@ export const getHeaderUrl = (user: User, config: Config) => {
if (config.media.backend === MediaBackendType.LOCAL) { if (config.media.backend === MediaBackendType.LOCAL) {
return `${config.http.base_url}/media/${user.header}`; return `${config.http.base_url}/media/${user.header}`;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (config.media.backend === MediaBackendType.S3) { }
if (config.media.backend === MediaBackendType.S3) {
return `${config.s3.public_url}/${user.header}`; return `${config.s3.public_url}/${user.header}`;
} }
return ""; return "";
@ -125,8 +127,8 @@ export const fetchRemoteUser = async (uri: string) => {
inbox: data.inbox, inbox: data.inbox,
outbox: data.outbox, outbox: data.outbox,
}, },
avatar: (data.avatar && data.avatar[0].content) || "", avatar: data.avatar?.[0].content || "",
header: (data.header && data.header[0].content) || "", header: data.header?.[0].content || "",
displayName: data.display_name ?? "", displayName: data.display_name ?? "",
note: data.bio?.[0].content ?? "", note: data.bio?.[0].content ?? "",
publicKey: data.public_key.public_key, publicKey: data.public_key.public_key,
@ -157,7 +159,7 @@ export const fetchRemoteUser = async (uri: string) => {
}, },
data: { data: {
emojis: { emojis: {
connect: emojis.map(emoji => ({ connect: emojis.map((emoji) => ({
id: emoji.id, id: emoji.id,
})), })),
}, },
@ -282,7 +284,7 @@ export const retrieveUserFromToken = async (access_token: string) => {
*/ */
export const getRelationshipToOtherUser = async ( export const getRelationshipToOtherUser = async (
user: UserWithRelations, user: UserWithRelations,
other: User other: User,
) => { ) => {
return await client.relationship.findFirst({ return await client.relationship.findFirst({
where: { where: {
@ -305,17 +307,17 @@ export const generateUserKeys = async () => {
String.fromCharCode.apply(null, [ String.fromCharCode.apply(null, [
...new Uint8Array( ...new Uint8Array(
// jesus help me what do these letters mean // jesus help me what do these letters mean
await crypto.subtle.exportKey("pkcs8", keys.privateKey) await crypto.subtle.exportKey("pkcs8", keys.privateKey),
), ),
]) ]),
); );
const publicKey = btoa( const publicKey = btoa(
String.fromCharCode( String.fromCharCode(
...new Uint8Array( ...new Uint8Array(
// why is exporting a key so hard // why is exporting a key so hard
await crypto.subtle.exportKey("spki", keys.publicKey) await crypto.subtle.exportKey("spki", keys.publicKey),
) ),
) ),
); );
// Add header, footer and newlines later on // Add header, footer and newlines later on
@ -328,7 +330,7 @@ export const generateUserKeys = async () => {
export const userToAPI = ( export const userToAPI = (
user: UserWithRelations, user: UserWithRelations,
isOwnAccount = false isOwnAccount = false,
): APIAccount => { ): APIAccount => {
return { return {
id: user.id, id: user.id,
@ -340,11 +342,11 @@ export const userToAPI = (
header: getHeaderUrl(user, config), header: getHeaderUrl(user, config),
locked: user.isLocked, locked: user.isLocked,
created_at: new Date(user.createdAt).toISOString(), created_at: new Date(user.createdAt).toISOString(),
followers_count: user.relationshipSubjects.filter(r => r.following) followers_count: user.relationshipSubjects.filter((r) => r.following)
.length, .length,
following_count: user.relationships.filter(r => r.following).length, following_count: user.relationships.filter((r) => r.following).length,
statuses_count: user._count.statuses, statuses_count: user._count.statuses,
emojis: user.emojis.map(emoji => emojiToAPI(emoji)), emojis: user.emojis.map((emoji) => emojiToAPI(emoji)),
// TODO: Add fields // TODO: Add fields
fields: [], fields: [],
bot: user.isBot, bot: user.isBot,
@ -419,7 +421,7 @@ export const userToLysand = (user: UserWithRelations): LysandUser => {
}, },
], ],
display_name: user.displayName, display_name: user.displayName,
fields: (user.source as any as APISource).fields.map(field => ({ fields: (user.source as APISource).fields.map((field) => ({
key: [ key: [
{ {
content: field.name, content: field.name,
@ -447,7 +449,7 @@ export const userToLysand = (user: UserWithRelations): LysandUser => {
}, },
extensions: { extensions: {
"org.lysand:custom_emojis": { "org.lysand:custom_emojis": {
emojis: user.emojis.map(emoji => emojiToLysand(emoji)), emojis: user.emojis.map((emoji) => emojiToLysand(emoji)),
}, },
}, },
}; };

View file

@ -1,21 +1,21 @@
import { exists, mkdir } from "node:fs/promises";
import { connectMeili } from "@meilisearch";
import { moduleIsEntry } from "@module";
import type { PrismaClientInitializationError } from "@prisma/client/runtime/library"; import type { PrismaClientInitializationError } from "@prisma/client/runtime/library";
import { initializeRedisCache } from "@redis"; import { initializeRedisCache } from "@redis";
import { connectMeili } from "@meilisearch";
import { config } from "config-manager"; import { config } from "config-manager";
import { client } from "~database/datasource";
import { LogLevel, LogManager, MultiLogManager } from "log-manager"; import { LogLevel, LogManager, MultiLogManager } from "log-manager";
import { moduleIsEntry } from "@module"; import { client } from "~database/datasource";
import { createServer } from "~server"; import { createServer } from "~server";
import { exists, mkdir } from "fs/promises";
const timeAtStart = performance.now(); const timeAtStart = performance.now();
const requests_log = Bun.file(process.cwd() + "/logs/requests.log"); const requests_log = Bun.file(`${process.cwd()}/logs/requests.log`);
const isEntry = moduleIsEntry(import.meta.url); const isEntry = moduleIsEntry(import.meta.url);
// If imported as a module, redirect logs to /dev/null to not pollute console (e.g. in tests) // If imported as a module, redirect logs to /dev/null to not pollute console (e.g. in tests)
const logger = new LogManager(isEntry ? requests_log : Bun.file(`/dev/null`)); const logger = new LogManager(isEntry ? requests_log : Bun.file("/dev/null"));
const consoleLogger = new LogManager( const consoleLogger = new LogManager(
isEntry ? Bun.stdout : Bun.file(`/dev/null`) isEntry ? Bun.stdout : Bun.file("/dev/null"),
); );
const dualLogger = new MultiLogManager([logger, consoleLogger]); const dualLogger = new MultiLogManager([logger, consoleLogger]);
@ -23,10 +23,10 @@ if (!(await exists(config.logging.storage.requests))) {
await consoleLogger.log( await consoleLogger.log(
LogLevel.WARNING, LogLevel.WARNING,
"Lysand", "Lysand",
`Creating logs directory at ${process.cwd()}/logs/` `Creating logs directory at ${process.cwd()}/logs/`,
); );
await mkdir(process.cwd() + "/logs/"); await mkdir(`${process.cwd()}/logs/`);
} }
await dualLogger.log(LogLevel.INFO, "Lysand", "Starting Lysand..."); await dualLogger.log(LogLevel.INFO, "Lysand", "Starting Lysand...");
@ -61,13 +61,15 @@ const server = createServer(config, dualLogger, isProd);
await dualLogger.log( await dualLogger.log(
LogLevel.INFO, LogLevel.INFO,
"Server", "Server",
`Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms` `Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(
performance.now() - timeAtStart
).toFixed(0)}ms`,
); );
await dualLogger.log( await dualLogger.log(
LogLevel.INFO, LogLevel.INFO,
"Database", "Database",
`Database is online, now serving ${postCount} posts` `Database is online, now serving ${postCount} posts`,
); );
export { config, server }; export { config, server };

View file

@ -14,11 +14,7 @@
}, },
"icon": "https://github.com/lysand-org/lysand", "icon": "https://github.com/lysand-org/lysand",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"keywords": [ "keywords": ["federated", "activitypub", "bun"],
"federated",
"activitypub",
"bun"
],
"workspaces": ["packages/*"], "workspaces": ["packages/*"],
"maintainers": [ "maintainers": [
{ {
@ -58,24 +54,16 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.6.4", "@biomejs/biome": "1.6.4",
"@julr/unocss-preset-forms": "^0.1.0", "@julr/unocss-preset-forms": "^0.1.0",
"@microsoft/eslint-formatter-sarif": "^3.0.0",
"@types/cli-table": "^0.3.4", "@types/cli-table": "^0.3.4",
"@types/html-to-text": "^9.0.4", "@types/html-to-text": "^9.0.4",
"@types/ioredis": "^5.0.0", "@types/ioredis": "^5.0.0",
"@types/jsonld": "^1.5.13", "@types/jsonld": "^1.5.13",
"@typescript-eslint/eslint-plugin": "latest", "@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"@unocss/cli": "latest", "@unocss/cli": "latest",
"@vitejs/plugin-vue": "latest", "@vitejs/plugin-vue": "latest",
"@vueuse/head": "^2.0.0", "@vueuse/head": "^2.0.0",
"activitypub-types": "^1.0.3", "activitypub-types": "^1.0.3",
"bun-types": "latest", "bun-types": "latest",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-formatter-pretty": "^6.0.0",
"eslint-formatter-summary": "^1.1.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0",
"typescript": "latest", "typescript": "latest",
"unocss": "latest", "unocss": "latest",
"untyped": "^1.4.2", "untyped": "^1.4.2",

View file

@ -1,8 +1,8 @@
import { CliParameterType, type CliParameter } from "./cli-builder.type";
import chalk from "chalk"; import chalk from "chalk";
import strip from "strip-ansi"; import strip from "strip-ansi";
import { type CliParameter, CliParameterType } from "./cli-builder.type";
export function startsWithArray(fullArray: any[], startArray: any[]) { export function startsWithArray(fullArray: string[], startArray: string[]) {
if (startArray.length > fullArray.length) { if (startArray.length > fullArray.length) {
return false; return false;
} }
@ -30,7 +30,9 @@ export class CliBuilder {
registerCommand(command: CliCommand) { registerCommand(command: CliCommand) {
if (this.checkIfCommandAlreadyExists(command)) { if (this.checkIfCommandAlreadyExists(command)) {
throw new Error( throw new Error(
`Command category '${command.categories.join(" ")}' already exists` `Command category '${command.categories.join(
" ",
)}' already exists`,
); );
} }
this.commands.push(command); this.commands.push(command);
@ -42,12 +44,14 @@ export class CliBuilder {
* @param commands Commands to add * @param commands Commands to add
*/ */
registerCommands(commands: CliCommand[]) { registerCommands(commands: CliCommand[]) {
const existingCommand = commands.find(command => const existingCommand = commands.find((command) =>
this.checkIfCommandAlreadyExists(command) this.checkIfCommandAlreadyExists(command),
); );
if (existingCommand) { if (existingCommand) {
throw new Error( throw new Error(
`Command category '${existingCommand.categories.join(" ")}' already exists` `Command category '${existingCommand.categories.join(
" ",
)}' already exists`,
); );
} }
this.commands.push(...commands); this.commands.push(...commands);
@ -59,7 +63,7 @@ export class CliBuilder {
*/ */
deregisterCommand(command: CliCommand) { deregisterCommand(command: CliCommand) {
this.commands = this.commands.filter( this.commands = this.commands.filter(
registeredCommand => registeredCommand !== command (registeredCommand) => registeredCommand !== command,
); );
} }
@ -69,18 +73,18 @@ export class CliBuilder {
*/ */
deregisterCommands(commands: CliCommand[]) { deregisterCommands(commands: CliCommand[]) {
this.commands = this.commands.filter( this.commands = this.commands.filter(
registeredCommand => !commands.includes(registeredCommand) (registeredCommand) => !commands.includes(registeredCommand),
); );
} }
checkIfCommandAlreadyExists(command: CliCommand) { checkIfCommandAlreadyExists(command: CliCommand) {
return this.commands.some( return this.commands.some(
registeredCommand => (registeredCommand) =>
registeredCommand.categories.length == registeredCommand.categories.length ===
command.categories.length && command.categories.length &&
registeredCommand.categories.every( registeredCommand.categories.every(
(category, index) => category === command.categories[index] (category, index) => category === command.categories[index],
) ),
); );
} }
@ -92,12 +96,12 @@ export class CliBuilder {
if (args[0].startsWith("./")) { if (args[0].startsWith("./")) {
// Formatted like ./cli.ts [command] // Formatted like ./cli.ts [command]
return args.slice(1); return args.slice(1);
} else if (args[0].includes("bun")) { }
if (args[0].includes("bun")) {
// Formatted like bun cli.ts [command] // Formatted like bun cli.ts [command]
return args.slice(2); return args.slice(2);
} else {
return args;
} }
return args;
} }
/** /**
@ -117,24 +121,28 @@ export class CliBuilder {
// Find revelant command // Find revelant command
// Search for a command with as many categories matching args as possible // Search for a command with as many categories matching args as possible
const matchingCommands = this.commands.filter(command => const matchingCommands = this.commands.filter((command) =>
startsWithArray(revelantArgs, command.categories) startsWithArray(revelantArgs, command.categories),
); );
if (matchingCommands.length === 0) { if (matchingCommands.length === 0) {
console.log( console.log(
`Invalid command "${revelantArgs.join(" ")}". Please use the ${chalk.bold("help")} command to see a list of commands` `Invalid command "${revelantArgs.join(
" ",
)}". Please use the ${chalk.bold(
"help",
)} command to see a list of commands`,
); );
return 0; return 0;
} }
// Get command with largest category size // Get command with largest category size
const command = matchingCommands.reduce((prev, current) => const command = matchingCommands.reduce((prev, current) =>
prev.categories.length > current.categories.length ? prev : current prev.categories.length > current.categories.length ? prev : current,
); );
const argsWithoutCategories = revelantArgs.slice( const argsWithoutCategories = revelantArgs.slice(
command.categories.length command.categories.length,
); );
return await command.run(argsWithoutCategories); return await command.run(argsWithoutCategories);
@ -210,34 +218,44 @@ export class CliBuilder {
const displayTree = (tree: TreeType, depth = 0) => { const displayTree = (tree: TreeType, depth = 0) => {
for (const [key, value] of Object.entries(tree)) { for (const [key, value] of Object.entries(tree)) {
if (value instanceof CliCommand) { if (value instanceof CliCommand) {
writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}|${chalk.underline(value.description)}\n`; writeBuffer += `${" ".repeat(depth)}${chalk.blue(
key,
)}|${chalk.underline(value.description)}\n`;
const positionedArgs = value.argTypes.filter( const positionedArgs = value.argTypes.filter(
arg => arg.positioned ?? true (arg) => arg.positioned ?? true,
); );
const unpositionedArgs = value.argTypes.filter( const unpositionedArgs = value.argTypes.filter(
arg => !(arg.positioned ?? true) (arg) => !(arg.positioned ?? true),
); );
for (const arg of positionedArgs) { for (const arg of positionedArgs) {
writeBuffer += `${" ".repeat(depth + 1)}${chalk.green( writeBuffer += `${" ".repeat(
arg.name depth + 1,
)}|${ )}${chalk.green(arg.name)}|${
arg.description ?? "(no description)" arg.description ?? "(no description)"
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`; } ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
} }
for (const arg of unpositionedArgs) { for (const arg of unpositionedArgs) {
writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}${arg.shortName ? ", " + chalk.yellow("-" + arg.shortName) : ""}|${ writeBuffer += `${" ".repeat(
arg.description ?? "(no description)" depth + 1,
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`; )}${chalk.yellow(`--${arg.name}`)}${
arg.shortName
? `, ${chalk.yellow(`-${arg.shortName}`)}`
: ""
}|${arg.description ?? "(no description)"} ${
arg.optional ? chalk.gray("(optional)") : ""
}\n`;
} }
if (value.example) { if (value.example) {
writeBuffer += `${" ".repeat(depth + 1)}${chalk.bold("Example:")} ${chalk.bgGray( writeBuffer += `${" ".repeat(depth + 1)}${chalk.bold(
value.example "Example:",
)}\n`; )} ${chalk.bgGray(value.example)}\n`;
} }
} else { } else {
writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}\n`; writeBuffer += `${" ".repeat(depth)}${chalk.blue(
key,
)}\n`;
displayTree(value, depth + 1); displayTree(value, depth + 1);
} }
} }
@ -247,8 +265,10 @@ export class CliBuilder {
// Replace all "|" with enough dots so that the text on the left + the dots = the same length // Replace all "|" with enough dots so that the text on the left + the dots = the same length
const optimal_length = Number( const optimal_length = Number(
// @ts-expect-error Slightly hacky but works writeBuffer
writeBuffer.split("\n").reduce((prev, current) => { .split("\n")
// @ts-expect-error I don't know how this works and I don't want to know
.reduce((prev, current) => {
// If previousValue is empty // If previousValue is empty
if (!prev) if (!prev)
return current.includes("|") return current.includes("|")
@ -257,8 +277,8 @@ export class CliBuilder {
if (!current.includes("|")) return prev; if (!current.includes("|")) return prev;
const [left] = current.split("|"); const [left] = current.split("|");
// Strip ANSI color codes or they mess up the length // Strip ANSI color codes or they mess up the length
return Math.max(Number(prev), strip(left).length); return Math.max(Number(prev), Bun.stringWidth(left));
}) }),
); );
for (const line of writeBuffer.split("\n")) { for (const line of writeBuffer.split("\n")) {
@ -268,7 +288,7 @@ export class CliBuilder {
continue; continue;
} }
// Strip ANSI color codes or they mess up the length // Strip ANSI color codes or they mess up the length
const dots = ".".repeat(optimal_length + 5 - strip(left).length); const dots = ".".repeat(optimal_length + 5 - Bun.stringWidth(left));
console.log(`${left}${dots}${right}`); console.log(`${left}${dots}${right}`);
} }
} }
@ -276,7 +296,7 @@ export class CliBuilder {
type ExecuteFunction<T> = ( type ExecuteFunction<T> = (
instance: CliCommand, instance: CliCommand,
args: Partial<T> args: Partial<T>,
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
) => Promise<number> | Promise<void> | number | void; ) => Promise<number> | Promise<void> | number | void;
@ -284,13 +304,15 @@ type ExecuteFunction<T> = (
* A command that can be executed from the command line * A command that can be executed from the command line
* @param categories Example: `["user", "create"]` for the command `./cli user create --name John` * @param categories Example: `["user", "create"]` for the command `./cli user create --name John`
*/ */
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export class CliCommand<T = any> { export class CliCommand<T = any> {
constructor( constructor(
public categories: string[], public categories: string[],
public argTypes: CliParameter[], public argTypes: CliParameter[],
private execute: ExecuteFunction<T>, private execute: ExecuteFunction<T>,
public description?: string, public description?: string,
public example?: string public example?: string,
) {} ) {}
/** /**
@ -299,10 +321,10 @@ export class CliCommand<T = any> {
*/ */
displayHelp() { displayHelp() {
const positionedArgs = this.argTypes.filter( const positionedArgs = this.argTypes.filter(
arg => arg.positioned ?? true (arg) => arg.positioned ?? true,
); );
const unpositionedArgs = this.argTypes.filter( const unpositionedArgs = this.argTypes.filter(
arg => !(arg.positioned ?? true) (arg) => !(arg.positioned ?? true),
); );
const helpMessage = ` const helpMessage = `
${chalk.green("📚 Command:")} ${chalk.yellow(this.categories.join(" "))} ${chalk.green("📚 Command:")} ${chalk.yellow(this.categories.join(" "))}
@ -310,22 +332,26 @@ ${this.description ? `${chalk.cyan(this.description)}\n` : ""}
${chalk.magenta("🔧 Arguments:")} ${chalk.magenta("🔧 Arguments:")}
${positionedArgs ${positionedArgs
.map( .map(
arg => (arg) =>
`${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${ `${chalk.bold(arg.name)}: ${chalk.blue(
arg.optional ? chalk.gray("(optional)") : "" arg.description ?? "(no description)",
}` )} ${arg.optional ? chalk.gray("(optional)") : ""}`,
) )
.join("\n")} .join("\n")}
${unpositionedArgs ${unpositionedArgs
.map( .map(
arg => (arg) =>
`--${chalk.bold(arg.name)}${arg.shortName ? `, -${arg.shortName}` : ""}: ${chalk.blue(arg.description ?? "(no description)")} ${ `--${chalk.bold(arg.name)}${
arg.shortName ? `, -${arg.shortName}` : ""
}: ${chalk.blue(arg.description ?? "(no description)")} ${
arg.optional ? chalk.gray("(optional)") : "" arg.optional ? chalk.gray("(optional)") : ""
}` }`,
) )
.join( .join("\n")}${
"\n" this.example
)}${this.example ? `\n${chalk.magenta("🚀 Example:")}\n${chalk.bgGray(this.example)}` : ""} ? `\n${chalk.magenta("🚀 Example:")}\n${chalk.bgGray(this.example)}`
: ""
}
`; `;
console.log(helpMessage); console.log(helpMessage);
@ -336,8 +362,11 @@ ${unpositionedArgs
* @param argsWithoutCategories * @param argsWithoutCategories
* @returns * @returns
*/ */
private parseArgs(argsWithoutCategories: string[]): Record<string, any> { private parseArgs(
const parsedArgs: Record<string, any> = {}; argsWithoutCategories: string[],
): Record<string, string | number | boolean | string[]> {
const parsedArgs: Record<string, string | number | boolean | string[]> =
{};
let currentParameter: CliParameter | null = null; let currentParameter: CliParameter | null = null;
for (let i = 0; i < argsWithoutCategories.length; i++) { for (let i = 0; i < argsWithoutCategories.length; i++) {
@ -346,15 +375,15 @@ ${unpositionedArgs
if (arg.startsWith("--")) { if (arg.startsWith("--")) {
const argName = arg.substring(2); const argName = arg.substring(2);
currentParameter = currentParameter =
this.argTypes.find(argType => argType.name === argName) || this.argTypes.find((argType) => argType.name === argName) ||
null; null;
if (currentParameter && !currentParameter.needsValue) { if (currentParameter && !currentParameter.needsValue) {
parsedArgs[argName] = true; parsedArgs[argName] = true;
currentParameter = null; currentParameter = null;
} else if (currentParameter && currentParameter.needsValue) { } else if (currentParameter?.needsValue) {
parsedArgs[argName] = this.castArgValue( parsedArgs[argName] = this.castArgValue(
argsWithoutCategories[i + 1], argsWithoutCategories[i + 1],
currentParameter.type currentParameter.type,
); );
i++; i++;
currentParameter = null; currentParameter = null;
@ -362,31 +391,32 @@ ${unpositionedArgs
} else if (arg.startsWith("-")) { } else if (arg.startsWith("-")) {
const shortName = arg.substring(1); const shortName = arg.substring(1);
const argType = this.argTypes.find( const argType = this.argTypes.find(
argType => argType.shortName === shortName (argType) => argType.shortName === shortName,
); );
if (argType && !argType.needsValue) { if (argType && !argType.needsValue) {
parsedArgs[argType.name] = true; parsedArgs[argType.name] = true;
} else if (argType && argType.needsValue) { } else if (argType?.needsValue) {
parsedArgs[argType.name] = this.castArgValue( parsedArgs[argType.name] = this.castArgValue(
argsWithoutCategories[i + 1], argsWithoutCategories[i + 1],
argType.type argType.type,
); );
i++; i++;
} }
} else if (currentParameter) { } else if (currentParameter) {
parsedArgs[currentParameter.name] = this.castArgValue( parsedArgs[currentParameter.name] = this.castArgValue(
arg, arg,
currentParameter.type currentParameter.type,
); );
currentParameter = null; currentParameter = null;
} else { } else {
const positionedArgType = this.argTypes.find( const positionedArgType = this.argTypes.find(
argType => argType.positioned && !parsedArgs[argType.name] (argType) =>
argType.positioned && !parsedArgs[argType.name],
); );
if (positionedArgType) { if (positionedArgType) {
parsedArgs[positionedArgType.name] = this.castArgValue( parsedArgs[positionedArgType.name] = this.castArgValue(
arg, arg,
positionedArgType.type positionedArgType.type,
); );
} }
} }
@ -395,7 +425,10 @@ ${unpositionedArgs
return parsedArgs; return parsedArgs;
} }
private castArgValue(value: string, type: CliParameter["type"]): any { private castArgValue(
value: string,
type: CliParameter["type"],
): string | number | boolean | string[] {
switch (type) { switch (type) {
case CliParameterType.STRING: case CliParameterType.STRING:
return value; return value;
@ -415,6 +448,6 @@ ${unpositionedArgs
*/ */
async run(argsWithoutCategories: string[]) { async run(argsWithoutCategories: string[]) {
const args = this.parseArgs(argsWithoutCategories); const args = this.parseArgs(argsWithoutCategories);
return await this.execute(this, args as any); return await this.execute(this, args as T);
} }
} }

View file

@ -1,7 +1,7 @@
// FILEPATH: /home/jessew/Dev/lysand/packages/cli-parser/index.test.ts import { beforeEach, describe, expect, it, jest, spyOn } from "bun:test";
import { CliCommand, CliBuilder, startsWithArray } from "..";
import { describe, beforeEach, it, expect, jest, spyOn } from "bun:test";
import stripAnsi from "strip-ansi"; import stripAnsi from "strip-ansi";
// FILEPATH: /home/jessew/Dev/lysand/packages/cli-parser/index.test.ts
import { CliBuilder, CliCommand, startsWithArray } from "..";
import { CliParameterType } from "../cli-builder.type"; import { CliParameterType } from "../cli-builder.type";
describe("startsWithArray", () => { describe("startsWithArray", () => {
@ -19,7 +19,7 @@ describe("startsWithArray", () => {
it("should return true when startArray is empty", () => { it("should return true when startArray is empty", () => {
const fullArray = ["a", "b", "c", "d", "e"]; const fullArray = ["a", "b", "c", "d", "e"];
const startArray: any[] = []; const startArray: string[] = [];
expect(startsWithArray(fullArray, startArray)).toBe(true); expect(startsWithArray(fullArray, startArray)).toBe(true);
}); });
@ -61,12 +61,13 @@ describe("CliCommand", () => {
], ],
() => { () => {
// Do nothing // Do nothing
} },
); );
}); });
it("should parse string arguments correctly", () => { it("should parse string arguments correctly", () => {
const args = cliCommand["parseArgs"]([ // @ts-expect-error Testing private method
const args = cliCommand.parseArgs([
"--arg1", "--arg1",
"value1", "value1",
"--arg2", "--arg2",
@ -84,7 +85,8 @@ describe("CliCommand", () => {
}); });
it("should parse short names for arguments too", () => { it("should parse short names for arguments too", () => {
const args = cliCommand["parseArgs"]([ // @ts-expect-error Testing private method
const args = cliCommand.parseArgs([
"--arg1", "--arg1",
"value1", "value1",
"-a", "-a",
@ -102,14 +104,15 @@ describe("CliCommand", () => {
}); });
it("should cast argument values correctly", () => { it("should cast argument values correctly", () => {
expect(cliCommand["castArgValue"]("42", CliParameterType.NUMBER)).toBe( // @ts-expect-error Testing private method
42 expect(cliCommand.castArgValue("42", CliParameterType.NUMBER)).toBe(42);
// @ts-expect-error Testing private method
expect(cliCommand.castArgValue("true", CliParameterType.BOOLEAN)).toBe(
true,
); );
expect( expect(
cliCommand["castArgValue"]("true", CliParameterType.BOOLEAN) // @ts-expect-error Testing private method
).toBe(true); cliCommand.castArgValue("value1,value2", CliParameterType.ARRAY),
expect(
cliCommand["castArgValue"]("value1,value2", CliParameterType.ARRAY)
).toEqual(["value1", "value2"]); ).toEqual(["value1", "value2"]);
}); });
@ -139,7 +142,7 @@ describe("CliCommand", () => {
needsValue: true, needsValue: true,
}, },
], ],
mockExecute mockExecute,
); );
await cliCommand.run([ await cliCommand.run([
@ -191,7 +194,7 @@ describe("CliCommand", () => {
positioned: true, positioned: true,
}, },
], ],
mockExecute mockExecute,
); );
await cliCommand.run([ await cliCommand.run([
@ -255,13 +258,13 @@ describe("CliCommand", () => {
// Do nothing // Do nothing
}, },
"This is a test command", "This is a test command",
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2" "category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2",
); );
cliCommand.displayHelp(); cliCommand.displayHelp();
const loggedString = consoleLogSpy.mock.calls.map(call => const loggedString = consoleLogSpy.mock.calls.map((call) =>
stripAnsi(call[0]) stripAnsi(call[0]),
)[0]; )[0];
consoleLogSpy.mockRestore(); consoleLogSpy.mockRestore();
@ -274,7 +277,7 @@ describe("CliCommand", () => {
expect(loggedString).toContain("--arg4: Argument 4"); expect(loggedString).toContain("--arg4: Argument 4");
expect(loggedString).toContain("🚀 Example:"); expect(loggedString).toContain("🚀 Example:");
expect(loggedString).toContain( expect(loggedString).toContain(
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2" "category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2",
); );
}); });
}); });
@ -336,7 +339,7 @@ describe("CliBuilder", () => {
positioned: false, positioned: false,
}, },
], ],
mockExecute mockExecute,
); );
cliBuilder.registerCommand(mockCommand); cliBuilder.registerCommand(mockCommand);
await cliBuilder.processArgs([ await cliBuilder.processArgs([
@ -365,7 +368,7 @@ describe("CliBuilder", () => {
mockCommand3 = new CliCommand( mockCommand3 = new CliCommand(
["user", "new", "admin"], ["user", "new", "admin"],
[], [],
jest.fn() jest.fn(),
); );
mockCommand4 = new CliCommand(["user", "new"], [], jest.fn()); mockCommand4 = new CliCommand(["user", "new"], [], jest.fn());
mockCommand5 = new CliCommand(["admin", "delete"], [], jest.fn()); mockCommand5 = new CliCommand(["admin", "delete"], [], jest.fn());
@ -450,36 +453,36 @@ describe("CliBuilder", () => {
// Do nothing // Do nothing
}, },
"I love sussy sauces", "I love sussy sauces",
"emoji add --url https://site.com/image.png" "emoji add --url https://site.com/image.png",
); );
cliBuilder.registerCommand(cliCommand); cliBuilder.registerCommand(cliCommand);
cliBuilder.displayHelp(); cliBuilder.displayHelp();
const loggedString = consoleLogSpy.mock.calls const loggedString = consoleLogSpy.mock.calls
.map(call => stripAnsi(call[0])) .map((call) => stripAnsi(call[0]))
.join("\n"); .join("\n");
consoleLogSpy.mockRestore(); consoleLogSpy.mockRestore();
expect(loggedString).toContain("category1"); expect(loggedString).toContain("category1");
expect(loggedString).toContain( expect(loggedString).toContain(
" category2.................I love sussy sauces" " category2.................I love sussy sauces",
); );
expect(loggedString).toContain( expect(loggedString).toContain(
" name..................Name of new item" " name..................Name of new item",
); );
expect(loggedString).toContain( expect(loggedString).toContain(
" arg3..................(no description)" " arg3..................(no description)",
); );
expect(loggedString).toContain( expect(loggedString).toContain(
" arg4..................(no description)" " arg4..................(no description)",
); );
expect(loggedString).toContain( expect(loggedString).toContain(
" --delete-previous.....Also delete the previous item (optional)" " --delete-previous.....Also delete the previous item (optional)",
); );
expect(loggedString).toContain( expect(loggedString).toContain(
" Example: emoji add --url https://site.com/image.png" " Example: emoji add --url https://site.com/image.png",
); );
}); });
}); });

View file

@ -78,7 +78,14 @@ export interface Config {
oidc: { oidc: {
/** @default [] */ /** @default [] */
providers: Record<string, any>[]; providers: {
name: string;
id: string;
url: string;
client_id: string;
client_secret: string;
icon: string;
}[];
}; };
http: { http: {
@ -91,9 +98,9 @@ export interface Config {
/** @default "8080" */ /** @default "8080" */
bind_port: string; bind_port: string;
banned_ips: any[]; banned_ips: string[];
banned_user_agents: any[]; banned_user_agents: string[];
bait: { bait: {
/** @default false */ /** @default false */
@ -229,7 +236,7 @@ export interface Config {
/** @default false */ /** @default false */
blacklist_tempmail: boolean; blacklist_tempmail: boolean;
email_blacklist: any[]; email_blacklist: string[];
/** @default ["http","https","ftp","dat","dweb","gopher","hyper","ipfs","ipns","irc","xmpp","ircs","magnet","mailto","mumble","ssb","gemini"] */ /** @default ["http","https","ftp","dat","dweb","gopher","hyper","ipfs","ipns","irc","xmpp","ircs","magnet","mailto","mumble","ssb","gemini"] */
url_scheme_whitelist: string[]; url_scheme_whitelist: string[];
@ -256,28 +263,28 @@ export interface Config {
}; };
federation: { federation: {
blocked: any[]; blocked: string[];
followers_only: any[]; followers_only: string[];
discard: { discard: {
reports: any[]; reports: string[];
deletes: any[]; deletes: string[];
updates: any[]; updates: string[];
media: any[]; media: string[];
follows: any[]; follows: string[];
likes: any[]; likes: string[];
reactions: any[]; reactions: string[];
banners: any[]; banners: string[];
avatars: any[]; avatars: string[];
}; };
}; };
@ -296,15 +303,15 @@ export interface Config {
}; };
filters: { filters: {
note_content: any[]; note_content: string[];
emoji: any[]; emoji: string[];
username: any[]; username: string[];
displayname: any[]; displayname: string[];
bio: any[]; bio: string[];
}; };
logging: { logging: {
@ -387,7 +394,7 @@ export const defaultConfig: Config = {
], ],
}, },
oidc: { oidc: {
providers: [[]], providers: [],
}, },
http: { http: {
base_url: "https://lysand.social", base_url: "https://lysand.social",

View file

@ -6,7 +6,7 @@
*/ */
import { watchConfig } from "c12"; import { watchConfig } from "c12";
import { defaultConfig, type Config } from "./config.type"; import { type Config, defaultConfig } from "./config.type";
const { config } = await watchConfig<Config>({ const { config } = await watchConfig<Config>({
configFile: "./config/config.toml", configFile: "./config/config.toml",

View file

@ -1,5 +1,5 @@
import { appendFile } from "node:fs/promises";
import type { BunFile } from "bun"; import type { BunFile } from "bun";
import { appendFile } from "fs/promises";
export enum LogLevel { export enum LogLevel {
DEBUG = "debug", DEBUG = "debug",
@ -16,7 +16,7 @@ export enum LogLevel {
export class LogManager { export class LogManager {
constructor(private output: BunFile) { constructor(private output: BunFile) {
void this.write( void this.write(
`--- INIT LogManager at ${new Date().toISOString()} ---` `--- INIT LogManager at ${new Date().toISOString()} ---`,
); );
} }
@ -31,16 +31,18 @@ export class LogManager {
level: LogLevel, level: LogLevel,
entity: string, entity: string,
message: string, message: string,
showTimestamp = true showTimestamp = true,
) { ) {
await this.write( await this.write(
`${showTimestamp ? new Date().toISOString() + " " : ""}[${level.toUpperCase()}] ${entity}: ${message}` `${
showTimestamp ? `${new Date().toISOString()} ` : ""
}[${level.toUpperCase()}] ${entity}: ${message}`,
); );
} }
private async write(text: string) { private async write(text: string) {
if (this.output == Bun.stdout) { if (this.output === Bun.stdout) {
await Bun.write(Bun.stdout, text + "\n"); await Bun.write(Bun.stdout, `${text}\n`);
} else { } else {
if (!(await this.output.exists())) { if (!(await this.output.exists())) {
// Create file if it doesn't exist // Create file if it doesn't exist
@ -48,7 +50,7 @@ export class LogManager {
createPath: true, createPath: true,
}); });
} }
await appendFile(this.output.name ?? "", text + "\n"); await appendFile(this.output.name ?? "", `${text}\n`);
} }
} }
@ -74,22 +76,22 @@ export class LogManager {
string += `${req.method} ${req.url}`; string += `${req.method} ${req.url}`;
if (logAllDetails) { if (logAllDetails) {
string += `\n`; string += "\n";
string += ` [Headers]\n`; string += " [Headers]\n";
// Pretty print headers // Pretty print headers
for (const [key, value] of req.headers.entries()) { for (const [key, value] of req.headers.entries()) {
string += ` ${key}: ${value}\n`; string += ` ${key}: ${value}\n`;
} }
// Pretty print body // Pretty print body
string += ` [Body]\n`; string += " [Body]\n";
const content_type = req.headers.get("Content-Type"); const content_type = req.headers.get("Content-Type");
if (content_type && content_type.includes("application/json")) { if (content_type?.includes("application/json")) {
const json = await req.json(); const json = await req.json();
const stringified = JSON.stringify(json, null, 4) const stringified = JSON.stringify(json, null, 4)
.split("\n") .split("\n")
.map(line => ` ${line}`) .map((line) => ` ${line}`)
.join("\n"); .join("\n");
string += `${stringified}\n`; string += `${stringified}\n`;
@ -103,7 +105,9 @@ export class LogManager {
if (value.toString().length < 300) { if (value.toString().length < 300) {
string += ` ${key}: ${value.toString()}\n`; string += ` ${key}: ${value.toString()}\n`;
} else { } else {
string += ` ${key}: <${value.toString().length} bytes>\n`; string += ` ${key}: <${
value.toString().length
} bytes>\n`;
} }
} }
} else { } else {
@ -132,7 +136,7 @@ export class MultiLogManager {
level: LogLevel, level: LogLevel,
entity: string, entity: string,
message: string, message: string,
showTimestamp = true showTimestamp = true,
) { ) {
for (const logManager of this.logManagers) { for (const logManager of this.logManagers) {
await logManager.log(level, entity, message, showTimestamp); await logManager.log(level, entity, message, showTimestamp);

View file

@ -2,5 +2,5 @@
"name": "log-manager", "name": "log-manager",
"version": "0.0.0", "version": "0.0.0",
"main": "index.ts", "main": "index.ts",
"dependencies": { } "dependencies": {}
} }

View file

@ -1,17 +1,17 @@
// FILEPATH: /home/jessew/Dev/lysand/packages/log-manager/log-manager.test.ts
import { LogManager, LogLevel, MultiLogManager } from "../index";
import type fs from "fs/promises";
import { import {
describe, type Mock,
it,
beforeEach, beforeEach,
describe,
expect, expect,
it,
jest, jest,
mock, mock,
type Mock,
test, test,
} from "bun:test"; } from "bun:test";
import type fs from "node:fs/promises";
import type { BunFile } from "bun"; import type { BunFile } from "bun";
// FILEPATH: /home/jessew/Dev/lysand/packages/log-manager/log-manager.test.ts
import { LogLevel, LogManager, MultiLogManager } from "../index";
describe("LogManager", () => { describe("LogManager", () => {
let logManager: LogManager; let logManager: LogManager;
@ -30,7 +30,7 @@ describe("LogManager", () => {
it("should initialize and write init log", () => { it("should initialize and write init log", () => {
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
expect.stringContaining("--- INIT LogManager at") expect.stringContaining("--- INIT LogManager at"),
); );
}); });
@ -38,7 +38,7 @@ describe("LogManager", () => {
await logManager.log(LogLevel.INFO, "TestEntity", "Test message"); await logManager.log(LogLevel.INFO, "TestEntity", "Test message");
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
expect.stringContaining("[INFO] TestEntity: Test message") expect.stringContaining("[INFO] TestEntity: Test message"),
); );
}); });
@ -47,11 +47,11 @@ describe("LogManager", () => {
LogLevel.INFO, LogLevel.INFO,
"TestEntity", "TestEntity",
"Test message", "Test message",
false false,
); );
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
"[INFO] TestEntity: Test message\n" "[INFO] TestEntity: Test message\n",
); );
}); });
@ -68,18 +68,18 @@ describe("LogManager", () => {
expect(writeMock).toHaveBeenCalledWith( expect(writeMock).toHaveBeenCalledWith(
Bun.stdout, Bun.stdout,
expect.stringContaining("[INFO] TestEntity: Test message") expect.stringContaining("[INFO] TestEntity: Test message"),
); );
}); });
it("should throw error if output file does not exist", () => { it("should throw error if output file does not exist", () => {
mockAppend.mockImplementationOnce(() => { mockAppend.mockImplementationOnce(() => {
return Promise.reject( return Promise.reject(
new Error("Output file doesnt exist (and isnt stdout)") new Error("Output file doesnt exist (and isnt stdout)"),
); );
}); });
expect( expect(
logManager.log(LogLevel.INFO, "TestEntity", "Test message") logManager.log(LogLevel.INFO, "TestEntity", "Test message"),
).rejects.toThrow(Error); ).rejects.toThrow(Error);
}); });
@ -88,7 +88,7 @@ describe("LogManager", () => {
await logManager.logError(LogLevel.ERROR, "TestEntity", error); await logManager.logError(LogLevel.ERROR, "TestEntity", error);
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
expect.stringContaining("[ERROR] TestEntity: Test error") expect.stringContaining("[ERROR] TestEntity: Test error"),
); );
}); });
@ -98,7 +98,7 @@ describe("LogManager", () => {
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
expect.stringContaining("127.0.0.1: GET http://localhost/test") expect.stringContaining("127.0.0.1: GET http://localhost/test"),
); );
}); });
@ -122,7 +122,7 @@ describe("LogManager", () => {
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
expect.stringContaining(expectedLog) expect.stringContaining(expectedLog),
); );
}); });
@ -142,7 +142,7 @@ describe("LogManager", () => {
`; `;
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
expect.stringContaining(expectedLog) expect.stringContaining(expectedLog),
); );
}); });
@ -167,8 +167,8 @@ describe("LogManager", () => {
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
expect.stringContaining( expect.stringContaining(
expectedLog.replace("----", expect.any(String)) expectedLog.replace("----", expect.any(String)),
) ),
); );
}); });
}); });
@ -207,7 +207,7 @@ describe("MultiLogManager", () => {
LogLevel.INFO, LogLevel.INFO,
"TestEntity", "TestEntity",
"Test message", "Test message",
true true,
); );
}); });
@ -218,7 +218,7 @@ describe("MultiLogManager", () => {
expect(mockLogError).toHaveBeenCalledWith( expect(mockLogError).toHaveBeenCalledWith(
LogLevel.ERROR, LogLevel.ERROR,
"TestEntity", "TestEntity",
error error,
); );
}); });

View file

@ -1,46 +1,47 @@
import type { Config } from "config-manager";
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
import type { ConvertableMediaFormats } from "../media-converter"; import type { ConvertableMediaFormats } from "../media-converter";
import { MediaConverter } from "../media-converter"; import { MediaConverter } from "../media-converter";
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
import type { ConfigType } from "config-manager";
export class LocalMediaBackend extends MediaBackend { export class LocalMediaBackend extends MediaBackend {
constructor(config: ConfigType) { constructor(config: Config) {
super(config, MediaBackendType.LOCAL); super(config, MediaBackendType.LOCAL);
} }
public async addFile(file: File) { public async addFile(file: File) {
let convertedFile = file;
if (this.shouldConvertImages(this.config)) { if (this.shouldConvertImages(this.config)) {
const fileExtension = file.name.split(".").pop(); const fileExtension = file.name.split(".").pop();
const mediaConverter = new MediaConverter( const mediaConverter = new MediaConverter(
fileExtension as ConvertableMediaFormats, fileExtension as ConvertableMediaFormats,
this.config.media.conversion this.config.media.conversion
.convert_to as ConvertableMediaFormats .convert_to as ConvertableMediaFormats,
); );
file = await mediaConverter.convert(file); convertedFile = await mediaConverter.convert(file);
} }
const hash = await new MediaHasher().getMediaHash(file); const hash = await new MediaHasher().getMediaHash(convertedFile);
const newFile = Bun.file( const newFile = Bun.file(
`${this.config.media.local_uploads_folder}/${hash}` `${this.config.media.local_uploads_folder}/${hash}`,
); );
if (await newFile.exists()) { if (await newFile.exists()) {
throw new Error("File already exists"); throw new Error("File already exists");
} }
await Bun.write(newFile, file); await Bun.write(newFile, convertedFile);
return { return {
uploadedFile: file, uploadedFile: convertedFile,
path: `./uploads/${file.name}`, path: `./uploads/${convertedFile.name}`,
hash: hash, hash: hash,
}; };
} }
public async getFileByHash( public async getFileByHash(
hash: string, hash: string,
databaseHashFetcher: (sha256: string) => Promise<string | null> databaseHashFetcher: (sha256: string) => Promise<string | null>,
): Promise<File | null> { ): Promise<File | null> {
const filename = await databaseHashFetcher(hash); const filename = await databaseHashFetcher(hash);
@ -51,7 +52,7 @@ export class LocalMediaBackend extends MediaBackend {
public async getFile(filename: string): Promise<File | null> { public async getFile(filename: string): Promise<File | null> {
const file = Bun.file( const file = Bun.file(
`${this.config.media.local_uploads_folder}/${filename}` `${this.config.media.local_uploads_folder}/${filename}`,
); );
if (!(await file.exists())) return null; if (!(await file.exists())) return null;

View file

@ -1,12 +1,12 @@
import { S3Client } from "@jsr/bradenmacdonald__s3-lite-client"; import { S3Client } from "@jsr/bradenmacdonald__s3-lite-client";
import type { Config } from "config-manager";
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
import type { ConvertableMediaFormats } from "../media-converter"; import type { ConvertableMediaFormats } from "../media-converter";
import { MediaConverter } from "../media-converter"; import { MediaConverter } from "../media-converter";
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
import type { ConfigType } from "config-manager";
export class S3MediaBackend extends MediaBackend { export class S3MediaBackend extends MediaBackend {
constructor( constructor(
config: ConfigType, config: Config,
private s3Client = new S3Client({ private s3Client = new S3Client({
endPoint: config.s3.endpoint, endPoint: config.s3.endpoint,
useSSL: true, useSSL: true,
@ -14,37 +14,42 @@ export class S3MediaBackend extends MediaBackend {
bucket: config.s3.bucket_name, bucket: config.s3.bucket_name,
accessKey: config.s3.access_key, accessKey: config.s3.access_key,
secretKey: config.s3.secret_access_key, secretKey: config.s3.secret_access_key,
}) }),
) { ) {
super(config, MediaBackendType.S3); super(config, MediaBackendType.S3);
} }
public async addFile(file: File) { public async addFile(file: File) {
let convertedFile = file;
if (this.shouldConvertImages(this.config)) { if (this.shouldConvertImages(this.config)) {
const fileExtension = file.name.split(".").pop(); const fileExtension = file.name.split(".").pop();
const mediaConverter = new MediaConverter( const mediaConverter = new MediaConverter(
fileExtension as ConvertableMediaFormats, fileExtension as ConvertableMediaFormats,
this.config.media.conversion this.config.media.conversion
.convert_to as ConvertableMediaFormats .convert_to as ConvertableMediaFormats,
); );
file = await mediaConverter.convert(file); convertedFile = await mediaConverter.convert(file);
} }
const hash = await new MediaHasher().getMediaHash(file); const hash = await new MediaHasher().getMediaHash(convertedFile);
await this.s3Client.putObject(file.name, file.stream(), { await this.s3Client.putObject(
size: file.size, convertedFile.name,
}); convertedFile.stream(),
{
size: convertedFile.size,
},
);
return { return {
uploadedFile: file, uploadedFile: convertedFile,
hash: hash, hash: hash,
}; };
} }
public async getFileByHash( public async getFileByHash(
hash: string, hash: string,
databaseHashFetcher: (sha256: string) => Promise<string | null> databaseHashFetcher: (sha256: string) => Promise<string | null>,
): Promise<File | null> { ): Promise<File | null> {
const filename = await databaseHashFetcher(hash); const filename = await databaseHashFetcher(hash);

View file

@ -1,4 +1,4 @@
import type { ConfigType } from "config-manager"; import type { Config } from "config-manager";
export enum MediaBackendType { export enum MediaBackendType {
LOCAL = "local", LOCAL = "local",
@ -28,25 +28,25 @@ export class MediaHasher {
export class MediaBackend { export class MediaBackend {
constructor( constructor(
public config: ConfigType, public config: Config,
public backend: MediaBackendType public backend: MediaBackendType,
) {} ) {}
static async fromBackendType( static async fromBackendType(
backend: MediaBackendType, backend: MediaBackendType,
config: ConfigType config: Config,
): Promise<MediaBackend> { ): Promise<MediaBackend> {
switch (backend) { switch (backend) {
case MediaBackendType.LOCAL: case MediaBackendType.LOCAL:
return new (await import("./backends/local")).LocalMediaBackend( return new (await import("./backends/local")).LocalMediaBackend(
config config,
); );
case MediaBackendType.S3: case MediaBackendType.S3:
return new (await import("./backends/s3")).S3MediaBackend( return new (await import("./backends/s3")).S3MediaBackend(
config config,
); );
default: default:
throw new Error(`Unknown backend type: ${backend as any}`); throw new Error(`Unknown backend type: ${backend as string}`);
} }
} }
@ -54,7 +54,7 @@ export class MediaBackend {
return this.backend; return this.backend;
} }
public shouldConvertImages(config: ConfigType) { public shouldConvertImages(config: Config) {
return config.media.conversion.convert_images; return config.media.conversion.convert_images;
} }
@ -68,10 +68,10 @@ export class MediaBackend {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
file: string, file: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
databaseHashFetcher: (sha256: string) => Promise<string> databaseHashFetcher: (sha256: string) => Promise<string>,
): Promise<File | null> { ): Promise<File | null> {
return Promise.reject( return Promise.reject(
new Error("Do not call MediaBackend directly: use a subclass") new Error("Do not call MediaBackend directly: use a subclass"),
); );
} }
@ -83,7 +83,7 @@ export class MediaBackend {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
public getFile(filename: string): Promise<File | null> { public getFile(filename: string): Promise<File | null> {
return Promise.reject( return Promise.reject(
new Error("Do not call MediaBackend directly: use a subclass") new Error("Do not call MediaBackend directly: use a subclass"),
); );
} }
@ -95,7 +95,7 @@ export class MediaBackend {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
public addFile(file: File): Promise<UploadedFileMetadata> { public addFile(file: File): Promise<UploadedFileMetadata> {
return Promise.reject( return Promise.reject(
new Error("Do not call MediaBackend directly: use a subclass") new Error("Do not call MediaBackend directly: use a subclass"),
); );
} }
} }

View file

@ -21,7 +21,7 @@ export enum ConvertableMediaFormats {
export class MediaConverter { export class MediaConverter {
constructor( constructor(
public fromFormat: ConvertableMediaFormats, public fromFormat: ConvertableMediaFormats,
public toFormat: ConvertableMediaFormats public toFormat: ConvertableMediaFormats,
) {} ) {}
/** /**
@ -43,7 +43,7 @@ export class MediaConverter {
private getReplacedFileName(fileName: string) { private getReplacedFileName(fileName: string) {
return this.extractFilenameFromPath(fileName).replace( return this.extractFilenameFromPath(fileName).replace(
new RegExp(`\\.${this.fromFormat}$`), new RegExp(`\\.${this.fromFormat}$`),
`.${this.toFormat}` `.${this.toFormat}`,
); );
} }

View file

@ -1,11 +1,11 @@
import { beforeEach, describe, expect, it, jest, spyOn } from "bun:test";
import type { S3Client } from "@jsr/bradenmacdonald__s3-lite-client";
import type { Config } from "config-manager";
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/backends/s3.test.ts // FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/backends/s3.test.ts
import { MediaBackend, MediaBackendType, MediaHasher } from ".."; import { MediaBackend, MediaBackendType, MediaHasher } from "..";
import type { S3Client } from "@bradenmacdonald/s3-lite-client";
import { beforeEach, describe, jest, it, expect, spyOn } from "bun:test";
import { S3MediaBackend } from "../backends/s3";
import type { ConfigType } from "config-manager";
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
import { LocalMediaBackend } from "../backends/local"; import { LocalMediaBackend } from "../backends/local";
import { S3MediaBackend } from "../backends/s3";
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
type DeepPartial<T> = { type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>; [P in keyof T]?: DeepPartial<T[P]>;
@ -13,7 +13,7 @@ type DeepPartial<T> = {
describe("MediaBackend", () => { describe("MediaBackend", () => {
let mediaBackend: MediaBackend; let mediaBackend: MediaBackend;
let mockConfig: ConfigType; let mockConfig: Config;
beforeEach(() => { beforeEach(() => {
mockConfig = { mockConfig = {
@ -22,7 +22,7 @@ describe("MediaBackend", () => {
convert_images: true, convert_images: true,
}, },
}, },
} as ConfigType; } as Config;
mediaBackend = new MediaBackend(mockConfig, MediaBackendType.S3); mediaBackend = new MediaBackend(mockConfig, MediaBackendType.S3);
}); });
@ -34,7 +34,7 @@ describe("MediaBackend", () => {
it("should return a LocalMediaBackend instance for LOCAL backend type", async () => { it("should return a LocalMediaBackend instance for LOCAL backend type", async () => {
const backend = await MediaBackend.fromBackendType( const backend = await MediaBackend.fromBackendType(
MediaBackendType.LOCAL, MediaBackendType.LOCAL,
mockConfig mockConfig,
); );
expect(backend).toBeInstanceOf(LocalMediaBackend); expect(backend).toBeInstanceOf(LocalMediaBackend);
}); });
@ -51,14 +51,15 @@ describe("MediaBackend", () => {
public_url: "test", public_url: "test",
secret_access_key: "test-secret", secret_access_key: "test-secret",
}, },
} as ConfigType } as Config,
); );
expect(backend).toBeInstanceOf(S3MediaBackend); expect(backend).toBeInstanceOf(S3MediaBackend);
}); });
it("should throw an error for unknown backend type", () => { it("should throw an error for unknown backend type", () => {
expect( expect(
MediaBackend.fromBackendType("unknown" as any, mockConfig) // @ts-expect-error This is a test
MediaBackend.fromBackendType("unknown", mockConfig),
).rejects.toThrow("Unknown backend type: unknown"); ).rejects.toThrow("Unknown backend type: unknown");
}); });
}); });
@ -74,7 +75,7 @@ describe("MediaBackend", () => {
const databaseHashFetcher = jest.fn().mockResolvedValue("test.jpg"); const databaseHashFetcher = jest.fn().mockResolvedValue("test.jpg");
expect( expect(
mediaBackend.getFileByHash(mockHash, databaseHashFetcher) mediaBackend.getFileByHash(mockHash, databaseHashFetcher),
).rejects.toThrow(Error); ).rejects.toThrow(Error);
}); });
@ -94,7 +95,7 @@ describe("MediaBackend", () => {
describe("S3MediaBackend", () => { describe("S3MediaBackend", () => {
let s3MediaBackend: S3MediaBackend; let s3MediaBackend: S3MediaBackend;
let mockS3Client: Partial<S3Client>; let mockS3Client: Partial<S3Client>;
let mockConfig: DeepPartial<ConfigType>; let mockConfig: DeepPartial<Config>;
let mockFile: File; let mockFile: File;
let mockMediaHasher: MediaHasher; let mockMediaHasher: MediaHasher;
@ -125,8 +126,8 @@ describe("S3MediaBackend", () => {
}), }),
} as Partial<S3Client>; } as Partial<S3Client>;
s3MediaBackend = new S3MediaBackend( s3MediaBackend = new S3MediaBackend(
mockConfig as ConfigType, mockConfig as Config,
mockS3Client as S3Client mockS3Client as S3Client,
); );
}); });
@ -145,7 +146,7 @@ describe("S3MediaBackend", () => {
expect(mockS3Client.putObject).toHaveBeenCalledWith( expect(mockS3Client.putObject).toHaveBeenCalledWith(
mockFile.name, mockFile.name,
expect.any(ReadableStream), expect.any(ReadableStream),
{ size: mockFile.size } { size: mockFile.size },
); );
}); });
@ -161,7 +162,7 @@ describe("S3MediaBackend", () => {
const file = await s3MediaBackend.getFileByHash( const file = await s3MediaBackend.getFileByHash(
mockHash, mockHash,
databaseHashFetcher databaseHashFetcher,
); );
expect(file).not.toBeNull(); expect(file).not.toBeNull();
@ -187,7 +188,7 @@ describe("S3MediaBackend", () => {
describe("LocalMediaBackend", () => { describe("LocalMediaBackend", () => {
let localMediaBackend: LocalMediaBackend; let localMediaBackend: LocalMediaBackend;
let mockConfig: ConfigType; let mockConfig: Config;
let mockFile: File; let mockFile: File;
let mockMediaHasher: MediaHasher; let mockMediaHasher: MediaHasher;
@ -200,15 +201,15 @@ describe("LocalMediaBackend", () => {
}, },
local_uploads_folder: "./uploads", local_uploads_folder: "./uploads",
}, },
} as ConfigType; } as Config;
mockFile = Bun.file(__dirname + "/megamind.jpg") as unknown as File; mockFile = Bun.file(`${__dirname}/megamind.jpg`) as unknown as File;
mockMediaHasher = new MediaHasher(); mockMediaHasher = new MediaHasher();
localMediaBackend = new LocalMediaBackend(mockConfig); localMediaBackend = new LocalMediaBackend(mockConfig);
}); });
it("should initialize with correct type", () => { it("should initialize with correct type", () => {
expect(localMediaBackend.getBackendType()).toEqual( expect(localMediaBackend.getBackendType()).toEqual(
MediaBackendType.LOCAL MediaBackendType.LOCAL,
); );
}); });
@ -217,7 +218,7 @@ describe("LocalMediaBackend", () => {
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash); spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
const mockMediaConverter = new MediaConverter( const mockMediaConverter = new MediaConverter(
ConvertableMediaFormats.JPG, ConvertableMediaFormats.JPG,
ConvertableMediaFormats.PNG ConvertableMediaFormats.PNG,
); );
spyOn(mockMediaConverter, "convert").mockResolvedValue(mockFile); spyOn(mockMediaConverter, "convert").mockResolvedValue(mockFile);
// @ts-expect-error This is a mock // @ts-expect-error This is a mock
@ -225,13 +226,13 @@ describe("LocalMediaBackend", () => {
exists: () => Promise.resolve(false), exists: () => Promise.resolve(false),
})); }));
spyOn(Bun, "write").mockImplementationOnce(() => spyOn(Bun, "write").mockImplementationOnce(() =>
Promise.resolve(mockFile.size) Promise.resolve(mockFile.size),
); );
const result = await localMediaBackend.addFile(mockFile); const result = await localMediaBackend.addFile(mockFile);
expect(result.uploadedFile).toEqual(mockFile); expect(result.uploadedFile).toEqual(mockFile);
expect(result.path).toEqual(`./uploads/megamind.png`); expect(result.path).toEqual("./uploads/megamind.png");
expect(result.hash).toHaveLength(64); expect(result.hash).toHaveLength(64);
}); });
@ -249,7 +250,7 @@ describe("LocalMediaBackend", () => {
const file = await localMediaBackend.getFileByHash( const file = await localMediaBackend.getFileByHash(
mockHash, mockHash,
databaseHashFetcher databaseHashFetcher,
); );
expect(file).not.toBeNull(); expect(file).not.toBeNull();

View file

@ -1,6 +1,6 @@
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/media-converter.test.ts // FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/media-converter.test.ts
import { describe, it, expect, beforeEach } from "bun:test"; import { beforeEach, describe, expect, it } from "bun:test";
import { MediaConverter, ConvertableMediaFormats } from "../media-converter"; import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
describe("MediaConverter", () => { describe("MediaConverter", () => {
let mediaConverter: MediaConverter; let mediaConverter: MediaConverter;
@ -8,7 +8,7 @@ describe("MediaConverter", () => {
beforeEach(() => { beforeEach(() => {
mediaConverter = new MediaConverter( mediaConverter = new MediaConverter(
ConvertableMediaFormats.JPG, ConvertableMediaFormats.JPG,
ConvertableMediaFormats.PNG ConvertableMediaFormats.PNG,
); );
}); });
@ -27,8 +27,8 @@ describe("MediaConverter", () => {
const fileName = "test.jpg"; const fileName = "test.jpg";
const expectedFileName = "test.png"; const expectedFileName = "test.png";
// Written like this because it's a private function // Written like this because it's a private function
expect(mediaConverter["getReplacedFileName"](fileName)).toEqual( expect(mediaConverter.getReplacedFileName(fileName)).toEqual(
expectedFileName expectedFileName,
); );
}); });
@ -36,30 +36,30 @@ describe("MediaConverter", () => {
it("should extract filename from path", () => { it("should extract filename from path", () => {
const path = "path/to/test.jpg"; const path = "path/to/test.jpg";
const expectedFileName = "test.jpg"; const expectedFileName = "test.jpg";
expect(mediaConverter["extractFilenameFromPath"](path)).toEqual( expect(mediaConverter.extractFilenameFromPath(path)).toEqual(
expectedFileName expectedFileName,
); );
}); });
it("should handle escaped slashes", () => { it("should handle escaped slashes", () => {
const path = "path/to/test\\/test.jpg"; const path = "path/to/test\\/test.jpg";
const expectedFileName = "test\\/test.jpg"; const expectedFileName = "test\\/test.jpg";
expect(mediaConverter["extractFilenameFromPath"](path)).toEqual( expect(mediaConverter.extractFilenameFromPath(path)).toEqual(
expectedFileName expectedFileName,
); );
}); });
}); });
it("should convert media", async () => { it("should convert media", async () => {
const file = Bun.file(__dirname + "/megamind.jpg"); const file = Bun.file(`${__dirname}/megamind.jpg`);
const convertedFile = await mediaConverter.convert( const convertedFile = await mediaConverter.convert(
file as unknown as File file as unknown as File,
); );
expect(convertedFile.name).toEqual("megamind.png"); expect(convertedFile.name).toEqual("megamind.png");
expect(convertedFile.type).toEqual( expect(convertedFile.type).toEqual(
`image/${ConvertableMediaFormats.PNG}` `image/${ConvertableMediaFormats.PNG}`,
); );
}); });
}); });

View file

@ -12,8 +12,9 @@ export enum SupportedProtocols {
* This class is not meant to be instantiated directly, but rather for its children to be used. * This class is not meant to be instantiated directly, but rather for its children to be used.
*/ */
export class ProtocolTranslator { export class ProtocolTranslator {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
static auto(object: any) { static auto(object: any) {
const protocol = this.recognizeProtocol(object); const protocol = ProtocolTranslator.recognizeProtocol(object);
switch (protocol) { switch (protocol) {
case SupportedProtocols.ACTIVITYPUB: case SupportedProtocols.ACTIVITYPUB:
return new ActivityPubTranslator(); return new ActivityPubTranslator();
@ -41,6 +42,8 @@ export class ProtocolTranslator {
/** /**
* Automatically recognizes the protocol of a given object * Automatically recognizes the protocol of a given object
*/ */
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
private static recognizeProtocol(object: any) { private static recognizeProtocol(object: any) {
// Temporary stub // Temporary stub
return SupportedProtocols.ACTIVITYPUB; return SupportedProtocols.ACTIVITYPUB;

View file

@ -1,11 +1,5 @@
import { ProtocolTranslator } from ".."; import { ProtocolTranslator } from "..";
export class ActivityPubTranslator extends ProtocolTranslator { export class ActivityPubTranslator extends ProtocolTranslator {
constructor() { user() {}
super();
}
user() {
}
} }

View file

@ -88,16 +88,16 @@ export class RequestParser {
for (const [key, value] of formData.entries()) { for (const [key, value] of formData.entries()) {
if (value instanceof File) { if (value instanceof File) {
result[key as keyof T] = value as any; result[key as keyof T] = value as T[keyof T];
} else if (key.endsWith("[]")) { } else if (key.endsWith("[]")) {
const arrayKey = key.slice(0, -2) as keyof T; const arrayKey = key.slice(0, -2) as keyof T;
if (!result[arrayKey]) { if (!result[arrayKey]) {
result[arrayKey] = [] as T[keyof T]; result[arrayKey] = [] as T[keyof T];
} }
(result[arrayKey] as any[]).push(value); (result[arrayKey] as FormDataEntryValue[]).push(value);
} else { } else {
result[key as keyof T] = value as any; result[key as keyof T] = value as T[keyof T];
} }
} }
@ -121,9 +121,9 @@ export class RequestParser {
result[arrayKey] = [] as T[keyof T]; result[arrayKey] = [] as T[keyof T];
} }
(result[arrayKey] as any[]).push(value); (result[arrayKey] as FormDataEntryValue[]).push(value);
} else { } else {
result[key as keyof T] = value as any; result[key as keyof T] = value as T[keyof T];
} }
} }
@ -162,7 +162,7 @@ export class RequestParser {
} }
(result[arrayKey] as string[]).push(value); (result[arrayKey] as string[]).push(value);
} else { } else {
result[key as keyof T] = value as any; result[key as keyof T] = value as T[keyof T];
} }
} }
return result; return result;

View file

@ -1,11 +1,11 @@
import { describe, it, expect, test } from "bun:test"; import { describe, expect, it, test } from "bun:test";
import { RequestParser } from ".."; import { RequestParser } from "..";
describe("RequestParser", () => { describe("RequestParser", () => {
describe("Should parse query parameters correctly", () => { describe("Should parse query parameters correctly", () => {
test("With text parameters", async () => { test("With text parameters", async () => {
const request = new Request( const request = new Request(
"http://localhost?param1=value1&param2=value2" "http://localhost?param1=value1&param2=value2",
); );
const result = await new RequestParser(request).toObject<{ const result = await new RequestParser(request).toObject<{
param1: string; param1: string;
@ -16,7 +16,7 @@ describe("RequestParser", () => {
test("With Array", async () => { test("With Array", async () => {
const request = new Request( const request = new Request(
"http://localhost?test[]=value1&test[]=value2" "http://localhost?test[]=value1&test[]=value2",
); );
const result = await new RequestParser(request).toObject<{ const result = await new RequestParser(request).toObject<{
test: string[]; test: string[];
@ -26,7 +26,7 @@ describe("RequestParser", () => {
test("With both at once", async () => { test("With both at once", async () => {
const request = new Request( const request = new Request(
"http://localhost?param1=value1&param2=value2&test[]=value1&test[]=value2" "http://localhost?param1=value1&param2=value2&test[]=value1&test[]=value2",
); );
const result = await new RequestParser(request).toObject<{ const result = await new RequestParser(request).toObject<{
param1: string; param1: string;

View file

@ -1,6 +1,6 @@
<script setup> <script setup>
// Import Tailwind style reset // Import Tailwind style reset
import '@unocss/reset/tailwind-compat.css' import "@unocss/reset/tailwind-compat.css";
</script> </script>
<template> <template>

View file

@ -10,7 +10,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from "vue";
const props = defineProps<{ const props = defineProps<{
label: string; label: string;
@ -26,5 +26,5 @@ const checkValid = (e: Event) => {
} else { } else {
isInvalid.value = true; isInvalid.value = true;
} }
} };
</script> </script>

View file

@ -1,9 +1,9 @@
import { createApp } from "vue";
import "./style.css";
import "virtual:uno.css"; import "virtual:uno.css";
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue"; import App from "./App.vue";
import routes from "./routes"; import routes from "./routes";
import "./style.css";
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),

View file

@ -37,7 +37,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from "vue";
const location = window.location; const location = window.location;
const version = __VERSION__; const version = __VERSION__;

View file

@ -52,9 +52,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router'; import { onMounted, ref } from "vue";
import LoginInput from "../../components/LoginInput.vue" import { useRoute } from "vue-router";
import { onMounted, ref } from 'vue'; import LoginInput from "../../components/LoginInput.vue";
const query = useRoute().query; const query = useRoute().query;
@ -64,18 +64,21 @@ const client_id = query.client_id;
const scope = query.scope; const scope = query.scope;
const error = decodeURIComponent(query.error as string); const error = decodeURIComponent(query.error as string);
const oauthProviders = ref<{ const oauthProviders = ref<
| {
name: string; name: string;
icon: string; icon: string;
id: string id: string;
}[] | null>(null); }[]
| null
>(null);
const getOauthProviders = async () => { const getOauthProviders = async () => {
const response = await fetch('/oauth/providers'); const response = await fetch("/oauth/providers");
return await response.json() as any; return await response.json();
} };
onMounted(async () => { onMounted(async () => {
oauthProviders.value = await getOauthProviders(); oauthProviders.value = await getOauthProviders();
}) });
</script> </script>

View file

@ -53,7 +53,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router'; import { useRoute } from "vue-router";
const query = useRoute().query; const query = useRoute().query;
@ -61,7 +61,7 @@ const application = query.application;
const website = decodeURIComponent(query.website as string); const website = decodeURIComponent(query.website as string);
const redirect_uri = query.redirect_uri as string; const redirect_uri = query.redirect_uri as string;
const client_id = query.client_id; const client_id = query.client_id;
const scope = decodeURIComponent(query.scope as string || ""); const scope = decodeURIComponent((query.scope as string) || "");
const code = query.code; const code = query.code;
const oauthScopeText: Record<string, string> = { const oauthScopeText: Record<string, string> = {
@ -79,7 +79,7 @@ const oauthScopeText: Record<string, string> = {
"w:conversations": "Edit your conversations", "w:conversations": "Edit your conversations",
"w:media": "Upload media", "w:media": "Upload media",
"w:reports": "Report users", "w:reports": "Report users",
} };
const scopes = scope.split(" "); const scopes = scope.split(" ");
@ -89,30 +89,56 @@ const scopes = scope.split(" ");
// Return an array of strings to display // Return an array of strings to display
// "read write:accounts" returns all the fields with $VERB as read, plus the accounts field with $VERB as write // "read write:accounts" returns all the fields with $VERB as read, plus the accounts field with $VERB as write
const getScopeText = (fullScopes: string[]) => { const getScopeText = (fullScopes: string[]) => {
let scopeTexts = []; const scopeTexts = [];
const readScopes = fullScopes.filter(scope => scope.includes("read")); const readScopes = fullScopes.filter((scope) => scope.includes("read"));
const writeScopes = fullScopes.filter(scope => scope.includes("write")); const writeScopes = fullScopes.filter((scope) => scope.includes("write"));
for (const possibleScope of Object.keys(oauthScopeText)) { for (const possibleScope of Object.keys(oauthScopeText)) {
const [scopeAction, scopeName] = possibleScope.split(':'); const [scopeAction, scopeName] = possibleScope.split(":");
if (scopeAction.includes("rw") && (readScopes.includes(`read:${scopeName}`) || readScopes.find(scope => scope === "read")) && (writeScopes.includes(`write:${scopeName}`) || writeScopes.find(scope => scope === "write"))) { if (
if (oauthScopeText[possibleScope].includes("$VERB")) scopeTexts.push(["Read and write", oauthScopeText[possibleScope].replace("$VERB", "")]); scopeAction.includes("rw") &&
(readScopes.includes(`read:${scopeName}`) ||
readScopes.find((scope) => scope === "read")) &&
(writeScopes.includes(`write:${scopeName}`) ||
writeScopes.find((scope) => scope === "write"))
) {
if (oauthScopeText[possibleScope].includes("$VERB"))
scopeTexts.push([
"Read and write",
oauthScopeText[possibleScope].replace("$VERB", ""),
]);
else scopeTexts.push(["", oauthScopeText[possibleScope]]); else scopeTexts.push(["", oauthScopeText[possibleScope]]);
continue; continue;
} }
if (scopeAction.includes('r') && (readScopes.includes(`read:${scopeName}`) || readScopes.find(scope => scope === "read"))) { if (
if (oauthScopeText[possibleScope].includes("$VERB")) scopeTexts.push(["Read", oauthScopeText[possibleScope].replace("$VERB", "")]); scopeAction.includes("r") &&
(readScopes.includes(`read:${scopeName}`) ||
readScopes.find((scope) => scope === "read"))
) {
if (oauthScopeText[possibleScope].includes("$VERB"))
scopeTexts.push([
"Read",
oauthScopeText[possibleScope].replace("$VERB", ""),
]);
else scopeTexts.push(["", oauthScopeText[possibleScope]]); else scopeTexts.push(["", oauthScopeText[possibleScope]]);
} }
if (scopeAction.includes('w') && (writeScopes.includes(`write:${scopeName}`) || writeScopes.find(scope => scope === "write"))) { if (
if (oauthScopeText[possibleScope].includes("$VERB")) scopeTexts.push(["Write", oauthScopeText[possibleScope].replace("$VERB", "")]); scopeAction.includes("w") &&
(writeScopes.includes(`write:${scopeName}`) ||
writeScopes.find((scope) => scope === "write"))
) {
if (oauthScopeText[possibleScope].includes("$VERB"))
scopeTexts.push([
"Write",
oauthScopeText[possibleScope].replace("$VERB", ""),
]);
else scopeTexts.push(["", oauthScopeText[possibleScope]]); else scopeTexts.push(["", oauthScopeText[possibleScope]]);
} }
} }
return scopeTexts; return scopeTexts;
} };
</script> </script>

View file

@ -98,12 +98,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from "vue";
import type { APIInstance } from "~types/entities/instance"; import type { APIInstance } from "~types/entities/instance";
import LoginInput from "../../components/LoginInput.vue" import LoginInput from "../../components/LoginInput.vue";
import { computed, ref, watch } from 'vue';
const instanceInfo = await fetch("/api/v1/instance").then(res => res.json()) as APIInstance & { const instanceInfo = (await fetch("/api/v1/instance").then((res) =>
tos_url: string res.json(),
)) as APIInstance & {
tos_url: string;
}; };
const errors = ref<{ const errors = ref<{
@ -124,26 +126,40 @@ const registerUser = (e: Event) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("email", (e.target as any).email.value); const target = e.target as unknown as Record<string, HTMLInputElement>;
formData.append("password", (e.target as any).password.value);
formData.append("username", (e.target as any).username.value); formData.append("email", target.email.value);
formData.append("password", target.password.value);
formData.append("username", target.username.value);
formData.append("reason", reason.value); formData.append("reason", reason.value);
formData.append("locale", "en") formData.append("locale", "en");
formData.append("agreement", "true"); formData.append("agreement", "true");
// @ts-ignore // @ts-ignore
fetch("/api/v1/accounts", { fetch("/api/v1/accounts", {
method: "POST", method: "POST",
body: formData, body: formData,
}).then(async res => { })
.then(async (res) => {
if (res.status === 422) { if (res.status === 422) {
errors.value = (await res.json() as any).details; errors.value = (
console.log(errors.value) (await res.json()) as Record<
string,
{
[key: string]: {
error: string;
description: string;
}[];
}
>
).details;
console.log(errors.value);
} else { } else {
// @ts-ignore // @ts-ignore
window.location.href = "/register/success"; window.location.href = "/register/success";
} }
}).catch(async err => {
console.error(err);
}) })
} .catch(async (err) => {
console.error(err);
});
};
</script> </script>

View file

@ -1,6 +1,6 @@
import { defineConfig } from "vite";
import UnoCSS from "unocss/vite";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import UnoCSS from "unocss/vite";
import { defineConfig } from "vite";
import pkg from "../package.json"; import pkg from "../package.json";
export default defineConfig({ export default defineConfig({

View file

@ -97,58 +97,58 @@ interface ServerEvents {
req: Request, req: Request,
obj: LysandObjectType, obj: LysandObjectType,
author: UserWithRelations, author: UserWithRelations,
publicKey: string publicKey: string,
) => void; ) => void;
[HookTypes.OnCryptoSuccess]: ( [HookTypes.OnCryptoSuccess]: (
req: Request, req: Request,
obj: LysandObjectType, obj: LysandObjectType,
author: UserWithRelations, author: UserWithRelations,
publicKey: string publicKey: string,
) => void; ) => void;
[HookTypes.OnBan]: ( [HookTypes.OnBan]: (
req: Request, req: Request,
bannedUser: UserWithRelations, bannedUser: UserWithRelations,
banner: UserWithRelations banner: UserWithRelations,
) => void; ) => void;
[HookTypes.OnSuspend]: ( [HookTypes.OnSuspend]: (
req: Request, req: Request,
suspendedUser: UserWithRelations, suspendedUser: UserWithRelations,
suspender: UserWithRelations suspender: UserWithRelations,
) => void; ) => void;
[HookTypes.OnUserBlock]: ( [HookTypes.OnUserBlock]: (
req: Request, req: Request,
blockedUser: UserWithRelations, blockedUser: UserWithRelations,
blocker: UserWithRelations blocker: UserWithRelations,
) => void; ) => void;
[HookTypes.OnUserMute]: ( [HookTypes.OnUserMute]: (
req: Request, req: Request,
mutedUser: UserWithRelations, mutedUser: UserWithRelations,
muter: UserWithRelations muter: UserWithRelations,
) => void; ) => void;
[HookTypes.OnUserFollow]: ( [HookTypes.OnUserFollow]: (
req: Request, req: Request,
followedUser: UserWithRelations, followedUser: UserWithRelations,
follower: UserWithRelations follower: UserWithRelations,
) => void; ) => void;
[HookTypes.OnRegister]: (req: Request, newUser: UserWithRelations) => void; [HookTypes.OnRegister]: (req: Request, newUser: UserWithRelations) => void;
[HookTypes.OnDeleteAccount]: ( [HookTypes.OnDeleteAccount]: (
req: Request, req: Request,
deletedUser: UserWithRelations deletedUser: UserWithRelations,
) => void; ) => void;
[HookTypes.OnPostCreate]: ( [HookTypes.OnPostCreate]: (
req: Request, req: Request,
newPost: StatusWithRelations, newPost: StatusWithRelations,
author: UserWithRelations author: UserWithRelations,
) => void; ) => void;
[HookTypes.OnPostDelete]: ( [HookTypes.OnPostDelete]: (
req: Request, req: Request,
deletedPost: StatusWithRelations, deletedPost: StatusWithRelations,
deleter: UserWithRelations deleter: UserWithRelations,
) => void; ) => void;
[HookTypes.OnPostUpdate]: ( [HookTypes.OnPostUpdate]: (
req: Request, req: Request,
updatedPost: StatusWithRelations, updatedPost: StatusWithRelations,
updater: UserWithRelations updater: UserWithRelations,
) => void; ) => void;
} }

View file

@ -3,7 +3,7 @@ import { config } from "config-manager";
// Proxies all `bunx prisma` commands with an environment variable // Proxies all `bunx prisma` commands with an environment variable
process.stdout.write( process.stdout.write(
`postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}\n` `postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}\n`,
); );
// Ends // Ends

View file

@ -93,7 +93,7 @@ export const rawRoutes = {
// Returns the route filesystem path when given a URL // Returns the route filesystem path when given a URL
export const routeMatcher = new Bun.FileSystemRouter({ export const routeMatcher = new Bun.FileSystemRouter({
style: "nextjs", style: "nextjs",
dir: process.cwd() + "/server/api", dir: `${process.cwd()}/server/api`,
}); });
export const matchRoute = async <T = Record<string, never>>(url: string) => { export const matchRoute = async <T = Record<string, never>>(url: string) => {

View file

@ -1,16 +1,16 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import type { Config } from "config-manager";
import { matches } from "ip-matching"; import { matches } from "ip-matching";
import { getFromRequest } from "~database/entities/User";
import { type Config } from "config-manager";
import type { LogManager, MultiLogManager } from "log-manager"; import type { LogManager, MultiLogManager } from "log-manager";
import { LogLevel } from "log-manager"; import { LogLevel } from "log-manager";
import { RequestParser } from "request-parser"; import { RequestParser } from "request-parser";
import { getFromRequest } from "~database/entities/User";
import { matchRoute } from "~routes"; import { matchRoute } from "~routes";
export const createServer = ( export const createServer = (
config: Config, config: Config,
logger: LogManager | MultiLogManager, logger: LogManager | MultiLogManager,
isProd: boolean isProd: boolean,
) => ) =>
Bun.serve({ Bun.serve({
port: config.http.bind_port, port: config.http.bind_port,
@ -52,22 +52,21 @@ export const createServer = (
if (matches(ip, request_ip)) { if (matches(ip, request_ip)) {
const file = Bun.file( const file = Bun.file(
config.http.bait.send_file || config.http.bait.send_file ||
"./pages/beemovie.txt" "./pages/beemovie.txt",
); );
if (await file.exists()) { if (await file.exists()) {
return new Response(file); return new Response(file);
} else { }
await logger.log( await logger.log(
LogLevel.ERROR, LogLevel.ERROR,
"Server.Bait", "Server.Bait",
`Bait file not found: ${config.http.bait.send_file}` `Bait file not found: ${config.http.bait.send_file}`,
); );
} }
}
} catch (e) { } catch (e) {
console.error( console.error(
`[-] Error while parsing bait IP "${ip}" ` `[-] Error while parsing bait IP "${ip}" `,
); );
throw e; throw e;
} }
@ -78,27 +77,27 @@ export const createServer = (
console.log(agent); console.log(agent);
if (new RegExp(agent).test(ua)) { if (new RegExp(agent).test(ua)) {
const file = Bun.file( const file = Bun.file(
config.http.bait.send_file || "./pages/beemovie.txt" config.http.bait.send_file ||
"./pages/beemovie.txt",
); );
if (await file.exists()) { if (await file.exists()) {
return new Response(file); return new Response(file);
} else { }
await logger.log( await logger.log(
LogLevel.ERROR, LogLevel.ERROR,
"Server.Bait", "Server.Bait",
`Bait file not found: ${config.http.bait.send_file}` `Bait file not found: ${config.http.bait.send_file}`,
); );
} }
} }
} }
}
if (config.logging.log_requests) { if (config.logging.log_requests) {
await logger.logRequest( await logger.logRequest(
req, req,
config.logging.log_ip ? request_ip : undefined, config.logging.log_ip ? request_ip : undefined,
config.logging.log_requests_verbose config.logging.log_requests_verbose,
); );
} }
@ -107,34 +106,31 @@ export const createServer = (
} }
const { file: filePromise, matchedRoute } = await matchRoute( const { file: filePromise, matchedRoute } = await matchRoute(
req.url req.url,
); );
const file = filePromise; const file = filePromise;
if (matchedRoute && file == undefined) { if (matchedRoute && file === undefined) {
await logger.log( await logger.log(
LogLevel.ERROR, LogLevel.ERROR,
"Server", "Server",
`Route file ${matchedRoute.filePath} not found or not registered in the routes file` `Route file ${matchedRoute.filePath} not found or not registered in the routes file`,
); );
return errorResponse("Route not found", 500); return errorResponse("Route not found", 500);
} }
if ( if (matchedRoute && matchedRoute.name !== "/[...404]" && file) {
matchedRoute &&
matchedRoute.name !== "/[...404]" &&
file != undefined
) {
const meta = file.meta; const meta = file.meta;
// Check for allowed requests // Check for allowed requests
if (!meta.allowedMethods.includes(req.method as any)) { // @ts-expect-error Stupid error
if (!meta.allowedMethods.includes(req.method as string)) {
return new Response(undefined, { return new Response(undefined, {
status: 405, status: 405,
statusText: `Method not allowed: allowed methods are: ${meta.allowedMethods.join( statusText: `Method not allowed: allowed methods are: ${meta.allowedMethods.join(
", " ", ",
)}`, )}`,
}); });
} }
@ -151,9 +147,8 @@ export const createServer = (
}); });
} }
} else if ( } else if (
(meta.auth.requiredOnMethods ?? []).includes( // @ts-expect-error Stupid error
req.method as any (meta.auth.requiredOnMethods ?? []).includes(req.method)
)
) { ) {
if (!auth.user) { if (!auth.user) {
return new Response(undefined, { return new Response(undefined, {
@ -171,7 +166,7 @@ export const createServer = (
await logger.logError( await logger.logError(
LogLevel.ERROR, LogLevel.ERROR,
"Server.RouteRequestParser", "Server.RouteRequestParser",
e as Error e as Error,
); );
return new Response(undefined, { return new Response(undefined, {
status: 400, status: 400,
@ -187,7 +182,8 @@ export const createServer = (
getConfig: () => Promise.resolve(config), getConfig: () => Promise.resolve(config),
}, },
}); });
} else if (matchedRoute?.name === "/[...404]" || !matchedRoute) { }
if (matchedRoute?.name === "/[...404]" || !matchedRoute) {
if (new URL(req.url).pathname.startsWith("/api")) { if (new URL(req.url).pathname.startsWith("/api")) {
return errorResponse("Route not found", 404); return errorResponse("Route not found", 404);
} }
@ -196,41 +192,42 @@ export const createServer = (
if (isProd) { if (isProd) {
if (new URL(req.url).pathname.startsWith("/assets")) { if (new URL(req.url).pathname.startsWith("/assets")) {
const file = Bun.file( const file = Bun.file(
`./pages/dist${new URL(req.url).pathname}` `./pages/dist${new URL(req.url).pathname}`,
); );
// Serve from pages/dist/assets // Serve from pages/dist/assets
if (await file.exists()) { if (await file.exists()) {
return new Response(file); return new Response(file);
} else return errorResponse("Asset not found", 404); }
return errorResponse("Asset not found", 404);
} }
if (new URL(req.url).pathname.startsWith("/api")) { if (new URL(req.url).pathname.startsWith("/api")) {
return errorResponse("Route not found", 404); return errorResponse("Route not found", 404);
} }
const file = Bun.file(`./pages/dist/index.html`); const file = Bun.file("./pages/dist/index.html");
// Serve from pages/dist // Serve from pages/dist
return new Response(file); return new Response(file);
} else { }
const proxy = await fetch( const proxy = await fetch(
req.url.replace( req.url.replace(
config.http.base_url, config.http.base_url,
"http://localhost:5173" "http://localhost:5173",
) ),
).catch(async e => { ).catch(async (e) => {
await logger.logError( await logger.logError(
LogLevel.ERROR, LogLevel.ERROR,
"Server.Proxy", "Server.Proxy",
e as Error e as Error,
); );
await logger.log( await logger.log(
LogLevel.ERROR, LogLevel.ERROR,
"Server.Proxy", "Server.Proxy",
`The development Vite server is not running or the route is not found: ${req.url.replace( `The development Vite server is not running or the route is not found: ${req.url.replace(
config.http.base_url, config.http.base_url,
"http://localhost:5173" "http://localhost:5173",
)}` )}`,
); );
return errorResponse("Route not found", 404); return errorResponse("Route not found", 404);
}); });
@ -244,8 +241,6 @@ export const createServer = (
return errorResponse("Route not found", 404); return errorResponse("Route not found", 404);
} }
} else {
return errorResponse("Route not found", 404); return errorResponse("Route not found", 404);
}
}, },
}); });

View file

@ -1,5 +1,5 @@
import { xmlResponse } from "@response";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { xmlResponse } from "@response";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -13,7 +13,6 @@ export const meta = applyConfig({
route: "/.well-known/host-meta", route: "/.well-known/host-meta",
}); });
export default apiRoute(async (req, matchedRoute, extraData) => { export default apiRoute(async (req, matchedRoute, extraData) => {
const config = await extraData.configManager.getConfig(); const config = await extraData.configManager.getConfig();

View file

@ -1,5 +1,5 @@
import { jsonResponse } from "@response";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -22,22 +22,28 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
name: config.instance.name, name: config.instance.name,
version: "0.0.1", version: "0.0.1",
description: config.instance.description, description: config.instance.description,
logo: config.instance.logo ? [ logo: config.instance.logo
? [
{ {
content: config.instance.logo, content: config.instance.logo,
content_type: `image/${config.instance.logo.split(".")[1]}`, content_type: `image/${
} config.instance.logo.split(".")[1]
] : undefined, }`,
banner: config.instance.banner ? [ },
]
: undefined,
banner: config.instance.banner
? [
{ {
content: config.instance.banner, content: config.instance.banner,
content_type: `image/${config.instance.banner.split(".")[1]}`, content_type: `image/${
} config.instance.banner.split(".")[1]
] : undefined, }`,
supported_extensions: [ },
"org.lysand:custom_emojis" ]
], : undefined,
supported_extensions: ["org.lysand:custom_emojis"],
website: "https://lysand.org", website: "https://lysand.org",
// TODO: Add admins, moderators field // TODO: Add admins, moderators field
}) });
}) });

View file

@ -12,7 +12,6 @@ export const meta = applyConfig({
route: "/.well-known/nodeinfo", route: "/.well-known/nodeinfo",
}); });
export default apiRoute(async (req, matchedRoute, extraData) => { export default apiRoute(async (req, matchedRoute, extraData) => {
const config = await extraData.configManager.getConfig(); const config = await extraData.configManager.getConfig();

View file

@ -1,5 +1,5 @@
import { errorResponse, jsonResponse } from "@response";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
@ -42,18 +42,18 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
{ {
rel: "self", rel: "self",
type: "application/activity+json", type: "application/activity+json",
href: `${config.http.base_url}/users/${user.username}/actor` href: `${config.http.base_url}/users/${user.username}/actor`,
}, },
{ {
rel: "https://webfinger.net/rel/profile-page", rel: "https://webfinger.net/rel/profile-page",
type: "text/html", type: "text/html",
href: `${config.http.base_url}/users/${user.username}` href: `${config.http.base_url}/users/${user.username}`,
}, },
{ {
rel: "self", rel: "self",
type: "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"", type: 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"',
href: `${config.http.base_url}/users/${user.username}/actor` href: `${config.http.base_url}/users/${user.username}/actor`,
} },
] ],
}) });
}); });

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,8 +1,8 @@
import { apiRoute, applyConfig } from "@api";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -68,15 +68,15 @@ export default apiRoute<{
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
await Promise.all(objects.map(object => userToAPI(object))), await Promise.all(objects.map((object) => userToAPI(object))),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -1,8 +1,8 @@
import { apiRoute, applyConfig } from "@api";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -68,15 +68,15 @@ export default apiRoute<{
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
await Promise.all(objects.map(object => userToAPI(object))), await Promise.all(objects.map((object) => userToAPI(object))),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -1,8 +1,8 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import type { UserWithRelations } from "~database/entities/User"; import type { UserWithRelations } from "~database/entities/User";
import { userToAPI } from "~database/entities/User"; import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { statusToAPI } from "~database/entities/Status";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { statusToAPI } from "~database/entities/Status";
import { import {
userRelations,
statusAndUserRelations, statusAndUserRelations,
userRelations,
} from "~database/entities/relations"; } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -84,16 +84,18 @@ export default apiRoute<{
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
await Promise.all(objects.map(status => statusToAPI(status, user))), await Promise.all(
objects.map((status) => statusToAPI(status, user)),
),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
} }
@ -120,15 +122,15 @@ export default apiRoute<{
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
await Promise.all(objects.map(status => statusToAPI(status, user))), await Promise.all(objects.map((status) => statusToAPI(status, user))),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { getRelationshipToOtherUser } from "~database/entities/User"; import { getRelationshipToOtherUser } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -54,7 +54,7 @@ export default apiRoute<{
some: { some: {
ownerId: self.id, ownerId: self.id,
subjectId: { subjectId: {
in: followersOfIds.map(f => f.id), in: followersOfIds.map((f) => f.id),
}, },
following: true, following: true,
}, },
@ -63,5 +63,5 @@ export default apiRoute<{
include: userRelations, include: userRelations,
}); });
return jsonResponse(output.map(o => userToAPI(o))); return jsonResponse(output.map((o) => userToAPI(o)));
}); });

View file

@ -1,9 +1,9 @@
import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { tempmailDomains } from "@tempmail"; import { tempmailDomains } from "@tempmail";
import { apiRoute, applyConfig } from "@api"; import ISO6391 from "iso-639-1";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { createNewLocalUser } from "~database/entities/User"; import { createNewLocalUser } from "~database/entities/User";
import ISO6391 from "iso-639-1";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -37,7 +37,7 @@ export default apiRoute<{
{ {
error: "Registration is disabled", error: "Registration is disabled",
}, },
422 422,
); );
} }
@ -70,33 +70,37 @@ export default apiRoute<{
}; };
// Check if fields are blank // Check if fields are blank
["username", "email", "password", "agreement", "locale", "reason"].forEach( for (const value of [
value => { "username",
// @ts-expect-error Value is always valid "email",
if (!body[value]) "password",
"agreement",
"locale",
"reason",
]) {
// @ts-expect-error We don't care about typing here
if (!body[value]) {
errors.details[value].push({ errors.details[value].push({
error: "ERR_BLANK", error: "ERR_BLANK",
description: `can't be blank`, description: `can't be blank`,
}); });
} }
); }
// Check if username is valid // Check if username is valid
if (!body.username?.match(/^[a-zA-Z0-9_]+$/)) if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
errors.details.username.push({ errors.details.username.push({
error: "ERR_INVALID", error: "ERR_INVALID",
description: `must only contain letters, numbers, and underscores`, description: "must only contain letters, numbers, and underscores",
}); });
// Check if username doesnt match filters // Check if username doesnt match filters
if ( if (
config.filters.username_filters.some(filter => config.filters.username.some((filter) => body.username?.match(filter))
body.username?.match(filter)
)
) { ) {
errors.details.username.push({ errors.details.username.push({
error: "ERR_INVALID", error: "ERR_INVALID",
description: `contains blocked words`, description: "contains blocked words",
}); });
} }
@ -111,32 +115,32 @@ export default apiRoute<{
if ((body.username?.length ?? 0) < 3) if ((body.username?.length ?? 0) < 3)
errors.details.username.push({ errors.details.username.push({
error: "ERR_TOO_SHORT", error: "ERR_TOO_SHORT",
description: `is too short (minimum is 3 characters)`, description: "is too short (minimum is 3 characters)",
}); });
// Check if username is reserved // Check if username is reserved
if (config.validation.username_blacklist.includes(body.username ?? "")) if (config.validation.username_blacklist.includes(body.username ?? ""))
errors.details.username.push({ errors.details.username.push({
error: "ERR_RESERVED", error: "ERR_RESERVED",
description: `is reserved`, description: "is reserved",
}); });
// Check if username is taken // Check if username is taken
if (await client.user.findFirst({ where: { username: body.username } })) if (await client.user.findFirst({ where: { username: body.username } }))
errors.details.username.push({ errors.details.username.push({
error: "ERR_TAKEN", error: "ERR_TAKEN",
description: `is already taken`, description: "is already taken",
}); });
// Check if email is valid // Check if email is valid
if ( if (
!body.email?.match( !body.email?.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
) )
) )
errors.details.email.push({ errors.details.email.push({
error: "ERR_INVALID", error: "ERR_INVALID",
description: `must be a valid email address`, description: "must be a valid email address",
}); });
// Check if email is blocked // Check if email is blocked
@ -147,14 +151,14 @@ export default apiRoute<{
) )
errors.details.email.push({ errors.details.email.push({
error: "ERR_BLOCKED", error: "ERR_BLOCKED",
description: `is from a blocked email provider`, description: "is from a blocked email provider",
}); });
// Check if agreement is accepted // Check if agreement is accepted
if (!body.agreement) if (!body.agreement)
errors.details.agreement.push({ errors.details.agreement.push({
error: "ERR_ACCEPTED", error: "ERR_ACCEPTED",
description: `must be accepted`, description: "must be accepted",
}); });
if (!body.locale) if (!body.locale)
@ -166,19 +170,19 @@ export default apiRoute<{
if (!ISO6391.validate(body.locale ?? "")) if (!ISO6391.validate(body.locale ?? ""))
errors.details.locale.push({ errors.details.locale.push({
error: "ERR_INVALID", error: "ERR_INVALID",
description: `must be a valid ISO 639-1 code`, description: "must be a valid ISO 639-1 code",
}); });
// If any errors are present, return them // If any errors are present, return them
if (Object.values(errors.details).some(value => value.length > 0)) { if (Object.values(errors.details).some((value) => value.length > 0)) {
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted" // Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
const errorsText = Object.entries(errors.details) const errorsText = Object.entries(errors.details)
.map( .map(
([name, errors]) => ([name, errors]) =>
`${name} ${errors `${name} ${errors
.map(error => error.description) .map((error) => error.description)
.join(", ")}` .join(", ")}`,
) )
.join(", "); .join(", ");
return jsonResponse( return jsonResponse(
@ -186,7 +190,7 @@ export default apiRoute<{
error: `Validation failed: ${errorsText}`, error: `Validation failed: ${errorsText}`,
details: errors.details, details: errors.details,
}, },
422 422,
); );
} }

View file

@ -1,10 +1,11 @@
import { apiRoute, applyConfig } from "@api";
import type { User } from "@prisma/client";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToAPI,
} from "~database/entities/Relationship"; } from "~database/entities/Relationship";
import { apiRoute, applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -47,20 +48,20 @@ export default apiRoute<{
// Find IDs that dont have a relationship // Find IDs that dont have a relationship
const missingIds = ids.filter( const missingIds = ids.filter(
id => !relationships.some(r => r.subjectId === id) (id) => !relationships.some((r) => r.subjectId === id),
); );
// Create the missing relationships // Create the missing relationships
for (const id of missingIds) { for (const id of missingIds) {
const relationship = await createNewRelationship(self, { id } as any); const relationship = await createNewRelationship(self, { id } as User);
relationships.push(relationship); relationships.push(relationship);
} }
// Order in the same order as ids // Order in the same order as ids
relationships.sort( relationships.sort(
(a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId) (a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId),
); );
return jsonResponse(relationships.map(r => relationshipToAPI(r))); return jsonResponse(relationships.map((r) => relationshipToAPI(r)));
}); });

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -71,5 +71,5 @@ export default apiRoute<{
include: userRelations, include: userRelations,
}); });
return jsonResponse(accounts.map(acct => userToAPI(acct))); return jsonResponse(accounts.map((acct) => userToAPI(acct)));
}); });

View file

@ -1,19 +1,19 @@
import { errorResponse, jsonResponse } from "@response";
import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { sanitize } from "isomorphic-dompurify"; import { convertTextToHtml } from "@formatting";
import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml } from "@sanitization"; import { sanitizeHtml } from "@sanitization";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { parseEmojis } from "~database/entities/Emoji"; import { sanitize } from "isomorphic-dompurify";
import { client } from "~database/datasource";
import type { APISource } from "~types/entities/source";
import { convertTextToHtml } from "@formatting";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import type { MediaBackend } from "media-manager"; import type { MediaBackend } from "media-manager";
import { client } from "~database/datasource";
import { getUrl } from "~database/entities/Attachment";
import { parseEmojis } from "~database/entities/Emoji";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations";
import { LocalMediaBackend } from "~packages/media-manager/backends/local"; import { LocalMediaBackend } from "~packages/media-manager/backends/local";
import { S3MediaBackend } from "~packages/media-manager/backends/s3"; import { S3MediaBackend } from "~packages/media-manager/backends/s3";
import { getUrl } from "~database/entities/Attachment"; import type { APISource } from "~types/entities/source";
import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["PATCH"], allowedMethods: ["PATCH"],
@ -97,14 +97,14 @@ export default apiRoute<{
) { ) {
return errorResponse( return errorResponse(
`Display name must be between 3 and ${config.validation.max_displayname_size} characters`, `Display name must be between 3 and ${config.validation.max_displayname_size} characters`,
422 422,
); );
} }
// Check if display name doesnt match filters // Check if display name doesnt match filters
if ( if (
config.filters.displayname.some(filter => config.filters.displayname.some((filter) =>
sanitizedDisplayName.match(filter) sanitizedDisplayName.match(filter),
) )
) { ) {
return errorResponse("Display name contains blocked words", 422); return errorResponse("Display name contains blocked words", 422);
@ -121,12 +121,12 @@ export default apiRoute<{
if (sanitizedNote.length > config.validation.max_note_size) { if (sanitizedNote.length > config.validation.max_note_size) {
return errorResponse( return errorResponse(
`Note must be less than ${config.validation.max_note_size} characters`, `Note must be less than ${config.validation.max_note_size} characters`,
422 422,
); );
} }
// Check if bio doesnt match filters // Check if bio doesnt match filters
if (config.filters.bio.some(filter => sanitizedNote.match(filter))) { if (config.filters.bio.some((filter) => sanitizedNote.match(filter))) {
return errorResponse("Bio contains blocked words", 422); return errorResponse("Bio contains blocked words", 422);
} }
@ -139,12 +139,12 @@ export default apiRoute<{
// Check if within allowed privacy values // Check if within allowed privacy values
if ( if (
!["public", "unlisted", "private", "direct"].includes( !["public", "unlisted", "private", "direct"].includes(
source_privacy source_privacy,
) )
) { ) {
return errorResponse( return errorResponse(
"Privacy must be one of public, unlisted, private, or direct", "Privacy must be one of public, unlisted, private, or direct",
422 422,
); );
} }
@ -164,7 +164,7 @@ export default apiRoute<{
if (!ISO6391.validate(source_language)) { if (!ISO6391.validate(source_language)) {
return errorResponse( return errorResponse(
"Language must be a valid ISO 639-1 code", "Language must be a valid ISO 639-1 code",
422 422,
); );
} }
@ -176,7 +176,7 @@ export default apiRoute<{
if (avatar.size > config.validation.max_avatar_size) { if (avatar.size > config.validation.max_avatar_size) {
return errorResponse( return errorResponse(
`Avatar must be less than ${config.validation.max_avatar_size} bytes`, `Avatar must be less than ${config.validation.max_avatar_size} bytes`,
422 422,
); );
} }
@ -190,7 +190,7 @@ export default apiRoute<{
if (header.size > config.validation.max_header_size) { if (header.size > config.validation.max_header_size) {
return errorResponse( return errorResponse(
`Header must be less than ${config.validation.max_avatar_size} bytes`, `Header must be less than ${config.validation.max_avatar_size} bytes`,
422 422,
); );
} }
@ -235,7 +235,8 @@ export default apiRoute<{
// Deduplicate emojis // Deduplicate emojis
user.emojis = user.emojis.filter( user.emojis = user.emojis.filter(
(emoji, index, self) => self.findIndex(e => e.id === emoji.id) === index (emoji, index, self) =>
self.findIndex((e) => e.id === emoji.id) === index,
); );
const output = await client.user.update({ const output = await client.user.update({
@ -249,10 +250,10 @@ export default apiRoute<{
isBot: user.isBot, isBot: user.isBot,
isDiscoverable: user.isDiscoverable, isDiscoverable: user.isDiscoverable,
emojis: { emojis: {
disconnect: user.emojis.map(e => ({ disconnect: user.emojis.map((e) => ({
id: e.id, id: e.id,
})), })),
connect: user.emojis.map(e => ({ connect: user.emojis.map((e) => ({
id: e.id, id: e.id,
})), })),
}, },

View file

@ -1,6 +1,6 @@
import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { userToAPI } from "~database/entities/User"; import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],

View file

@ -1,6 +1,6 @@
import { randomBytes } from "node:crypto";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { randomBytes } from "crypto";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
@ -35,7 +35,7 @@ export default apiRoute<{
if (!redirect_uri.protocol.startsWith("http")) { if (!redirect_uri.protocol.startsWith("http")) {
return errorResponse( return errorResponse(
"Redirect URI must be an absolute URI", "Redirect URI must be an absolute URI",
422 422,
); );
} }
} catch { } catch {

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -33,5 +33,5 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
include: userRelations, include: userRelations,
}); });
return jsonResponse(blocks.map(u => userToAPI(u))); return jsonResponse(blocks.map((u) => userToAPI(u)));
}); });

View file

@ -23,6 +23,6 @@ export default apiRoute(async () => {
}); });
return jsonResponse( return jsonResponse(
await Promise.all(emojis.map(emoji => emojiToAPI(emoji))) await Promise.all(emojis.map((emoji) => emojiToAPI(emoji))),
); );
}); });

View file

@ -1,8 +1,8 @@
import { errorResponse, jsonResponse } from "@response";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { statusAndUserRelations } from "~database/entities/relations";
import { statusToAPI } from "~database/entities/Status"; import { statusToAPI } from "~database/entities/Status";
import { statusAndUserRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -58,17 +58,17 @@ export default apiRoute<{
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
await Promise.all( await Promise.all(
objects.map(async status => statusToAPI(status, user)) objects.map(async (status) => statusToAPI(status, user)),
), ),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -1,5 +1,5 @@
import { errorResponse, jsonResponse } from "@response";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { import {
checkForBidirectionalRelationships, checkForBidirectionalRelationships,

View file

@ -1,5 +1,5 @@
import { errorResponse, jsonResponse } from "@response";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { import {
checkForBidirectionalRelationships, checkForBidirectionalRelationships,

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -59,15 +59,15 @@ export default apiRoute<{
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
objects.map(user => userToAPI(user)), objects.map((user) => userToAPI(user)),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -2,9 +2,9 @@ import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User"; import { userToAPI } from "~database/entities/User";
import type { APIInstance } from "~types/entities/instance";
import manifest from "~package.json";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
import manifest from "~package.json";
import type { APIInstance } from "~types/entities/instance";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],

View file

@ -1,9 +1,9 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
import type { MediaBackend } from "media-manager"; import type { MediaBackend } from "media-manager";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import { client } from "~database/datasource";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
import { LocalMediaBackend } from "~packages/media-manager/backends/local"; import { LocalMediaBackend } from "~packages/media-manager/backends/local";
import { S3MediaBackend } from "~packages/media-manager/backends/s3"; import { S3MediaBackend } from "~packages/media-manager/backends/s3";
@ -52,12 +52,11 @@ export default apiRoute<{
case "GET": { case "GET": {
if (attachment.url) { if (attachment.url) {
return jsonResponse(attachmentToAPI(attachment)); return jsonResponse(attachmentToAPI(attachment));
} else { }
return new Response(null, { return new Response(null, {
status: 206, status: 206,
}); });
} }
}
case "PUT": { case "PUT": {
const { description, thumbnail } = extraData.parsedRequest; const { description, thumbnail } = extraData.parsedRequest;

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { encode } from "blurhash"; import { encode } from "blurhash";
import sharp from "sharp";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import type { MediaBackend } from "media-manager"; import type { MediaBackend } from "media-manager";
import sharp from "sharp";
import { client } from "~database/datasource";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
import { LocalMediaBackend } from "~packages/media-manager/backends/local"; import { LocalMediaBackend } from "~packages/media-manager/backends/local";
import { S3MediaBackend } from "~packages/media-manager/backends/s3"; import { S3MediaBackend } from "~packages/media-manager/backends/s3";
@ -49,7 +49,7 @@ export default apiRoute<{
if (file.size > config.validation.max_media_size) { if (file.size > config.validation.max_media_size) {
return errorResponse( return errorResponse(
`File too large, max size is ${config.validation.max_media_size} bytes`, `File too large, max size is ${config.validation.max_media_size} bytes`,
413 413,
); );
} }
@ -66,7 +66,7 @@ export default apiRoute<{
) { ) {
return errorResponse( return errorResponse(
`Description too long, max length is ${config.validation.max_media_description_size} characters`, `Description too long, max length is ${config.validation.max_media_description_size} characters`,
413 413,
); );
} }
@ -84,7 +84,7 @@ export default apiRoute<{
metadata?.width ?? 0, metadata?.width ?? 0,
metadata?.height ?? 0, metadata?.height ?? 0,
4, 4,
4 4,
) )
: null; : null;

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response";
import { userToAPI } from "~database/entities/User";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -33,5 +33,5 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
include: userRelations, include: userRelations,
}); });
return jsonResponse(blocks.map(u => userToAPI(u))); return jsonResponse(blocks.map((u) => userToAPI(u)));
}); });

View file

@ -1,10 +1,10 @@
import { errorResponse, jsonResponse } from "@response";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { notificationToAPI } from "~database/entities/Notification"; import { notificationToAPI } from "~database/entities/Notification";
import { import {
userRelations,
statusAndUserRelations, statusAndUserRelations,
userRelations,
} from "~database/entities/relations"; } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -83,20 +83,20 @@ export default apiRoute<{
if (objects.length > 0) { if (objects.length > 0) {
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"` `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`,
); );
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?since_id=${ `<${urlWithoutQuery}?since_id=${
objects.at(-1)?.id objects.at(-1)?.id
}&limit=${limit}>; rel="prev"` }&limit=${limit}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
await Promise.all(objects.map(n => notificationToAPI(n))), await Promise.all(objects.map((n) => notificationToAPI(n))),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -43,10 +43,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
return jsonResponse({ return jsonResponse({
ancestors: await Promise.all( ancestors: await Promise.all(
ancestors.map(status => statusToAPI(status, user || undefined)) ancestors.map((status) => statusToAPI(status, user || undefined)),
), ),
descendants: await Promise.all( descendants: await Promise.all(
descendants.map(status => statusToAPI(status, user || undefined)) descendants.map((status) => statusToAPI(status, user || undefined)),
), ),
}); });
}); });

View file

@ -85,20 +85,20 @@ export default apiRoute<{
if (objects.length > 0) { if (objects.length > 0) {
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"` `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`,
); );
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?since_id=${ `<${urlWithoutQuery}?since_id=${
objects[objects.length - 1].id objects[objects.length - 1].id
}&limit=${limit}>; rel="prev"` }&limit=${limit}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
objects.map(user => userToAPI(user)), objects.map((user) => userToAPI(user)),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -55,7 +55,8 @@ export default apiRoute<{
if (req.method === "GET") { if (req.method === "GET") {
return jsonResponse(await statusToAPI(status)); return jsonResponse(await statusToAPI(status));
} else if (req.method === "DELETE") { }
if (req.method === "DELETE") {
if (status.authorId !== user?.id) { if (status.authorId !== user?.id) {
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
@ -77,9 +78,10 @@ export default apiRoute<{
// poll: Add source poll // poll: Add source poll
// media_attachments // media_attachments
}, },
200 200,
); );
} else if (req.method == "PUT") { }
if (req.method === "PUT") {
if (status.authorId !== user?.id) { if (status.authorId !== user?.id) {
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
@ -99,7 +101,7 @@ export default apiRoute<{
if (!statusText && !(media_ids && media_ids.length > 0)) { if (!statusText && !(media_ids && media_ids.length > 0)) {
return errorResponse( return errorResponse(
"Status is required unless media is attached", "Status is required unless media is attached",
422 422,
); );
} }
@ -130,33 +132,33 @@ export default apiRoute<{
if (options && options.length > config.validation.max_poll_options) { if (options && options.length > config.validation.max_poll_options) {
return errorResponse( return errorResponse(
`Poll options must be less than ${config.validation.max_poll_options}`, `Poll options must be less than ${config.validation.max_poll_options}`,
422 422,
); );
} }
if ( if (
options && options?.some(
options.some( (option) =>
option => option.length > config.validation.max_poll_option_size option.length > config.validation.max_poll_option_size,
) )
) { ) {
return errorResponse( return errorResponse(
`Poll options must be less than ${config.validation.max_poll_option_size} characters`, `Poll options must be less than ${config.validation.max_poll_option_size} characters`,
422 422,
); );
} }
if (expires_in && expires_in < config.validation.min_poll_duration) { if (expires_in && expires_in < config.validation.min_poll_duration) {
return errorResponse( return errorResponse(
`Poll duration must be greater than ${config.validation.min_poll_duration} seconds`, `Poll duration must be greater than ${config.validation.min_poll_duration} seconds`,
422 422,
); );
} }
if (expires_in && expires_in > config.validation.max_poll_duration) { if (expires_in && expires_in > config.validation.max_poll_duration) {
return errorResponse( return errorResponse(
`Poll duration must be less than ${config.validation.max_poll_duration} seconds`, `Poll duration must be less than ${config.validation.max_poll_duration} seconds`,
422 422,
); );
} }
@ -175,14 +177,14 @@ export default apiRoute<{
if (sanitizedStatus.length > config.validation.max_note_size) { if (sanitizedStatus.length > config.validation.max_note_size) {
return errorResponse( return errorResponse(
`Status must be less than ${config.validation.max_note_size} characters`, `Status must be less than ${config.validation.max_note_size} characters`,
400 400,
); );
} }
// Check if status body doesnt match filters // Check if status body doesnt match filters
if ( if (
config.filters.note_content.some(filter => config.filters.note_content.some((filter) =>
statusText?.match(filter) statusText?.match(filter),
) )
) { ) {
return errorResponse("Status contains blocked words", 422); return errorResponse("Status contains blocked words", 422);

View file

@ -3,7 +3,7 @@ import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { isViewableByUser, statusToAPI } from "~database/entities/Status"; import { isViewableByUser, statusToAPI } from "~database/entities/Status";
import { type UserWithRelations } from "~database/entities/User"; import type { UserWithRelations } from "~database/entities/User";
import { statusAndUserRelations } from "~database/entities/relations"; import { statusAndUserRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -91,7 +91,7 @@ export default apiRoute<{
...newReblog, ...newReblog,
uri: `${config.http.base_url}/statuses/${newReblog.id}`, uri: `${config.http.base_url}/statuses/${newReblog.id}`,
}, },
user user,
) ),
); );
}); });

View file

@ -86,20 +86,20 @@ export default apiRoute<{
if (objects.length > 0) { if (objects.length > 0) {
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"` `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`,
); );
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?since_id=${ `<${urlWithoutQuery}?since_id=${
objects[objects.length - 1].id objects[objects.length - 1].id
}&limit=${limit}>; rel="prev"` }&limit=${limit}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
objects.map(user => userToAPI(user)), objects.map((user) => userToAPI(user)),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -69,7 +69,7 @@ export default apiRoute<{
if (!status && !(media_ids && media_ids.length > 0)) { if (!status && !(media_ids && media_ids.length > 0)) {
return errorResponse( return errorResponse(
"Status is required unless media is attached", "Status is required unless media is attached",
422 422,
); );
} }
@ -100,33 +100,32 @@ export default apiRoute<{
if (options && options.length > config.validation.max_poll_options) { if (options && options.length > config.validation.max_poll_options) {
return errorResponse( return errorResponse(
`Poll options must be less than ${config.validation.max_poll_options}`, `Poll options must be less than ${config.validation.max_poll_options}`,
422 422,
); );
} }
if ( if (
options && options?.some(
options.some( (option) => option.length > config.validation.max_poll_option_size,
option => option.length > config.validation.max_poll_option_size
) )
) { ) {
return errorResponse( return errorResponse(
`Poll options must be less than ${config.validation.max_poll_option_size} characters`, `Poll options must be less than ${config.validation.max_poll_option_size} characters`,
422 422,
); );
} }
if (expires_in && expires_in < config.validation.min_poll_duration) { if (expires_in && expires_in < config.validation.min_poll_duration) {
return errorResponse( return errorResponse(
`Poll duration must be greater than ${config.validation.min_poll_duration} seconds`, `Poll duration must be greater than ${config.validation.min_poll_duration} seconds`,
422 422,
); );
} }
if (expires_in && expires_in > config.validation.max_poll_duration) { if (expires_in && expires_in > config.validation.max_poll_duration) {
return errorResponse( return errorResponse(
`Poll duration must be less than ${config.validation.max_poll_duration} seconds`, `Poll duration must be less than ${config.validation.max_poll_duration} seconds`,
422 422,
); );
} }
@ -139,11 +138,11 @@ export default apiRoute<{
let sanitizedStatus: string; let sanitizedStatus: string;
if (content_type === "text/markdown") { if (content_type === "text/markdown") {
sanitizedStatus = await sanitizeHtml(parse(status ?? "") as any); sanitizedStatus = await sanitizeHtml(parse(status ?? "") as string);
} else if (content_type === "text/x.misskeymarkdown") { } else if (content_type === "text/x.misskeymarkdown") {
// Parse as MFM // Parse as MFM
// TODO: Parse as MFM // TODO: Parse as MFM
sanitizedStatus = await sanitizeHtml(parse(status ?? "") as any); sanitizedStatus = await sanitizeHtml(parse(status ?? "") as string);
} else { } else {
sanitizedStatus = await sanitizeHtml(status ?? ""); sanitizedStatus = await sanitizeHtml(status ?? "");
} }
@ -151,7 +150,7 @@ export default apiRoute<{
if (sanitizedStatus.length > config.validation.max_note_size) { if (sanitizedStatus.length > config.validation.max_note_size) {
return errorResponse( return errorResponse(
`Status must be less than ${config.validation.max_note_size} characters`, `Status must be less than ${config.validation.max_note_size} characters`,
400 400,
); );
} }
@ -194,7 +193,7 @@ export default apiRoute<{
} }
// Check if status body doesnt match filters // Check if status body doesnt match filters
if (config.filters.note_content.some(filter => status?.match(filter))) { if (config.filters.note_content.some((filter) => status?.match(filter))) {
return errorResponse("Status contains blocked words", 422); return errorResponse("Status contains blocked words", 422);
} }

View file

@ -83,17 +83,17 @@ export default apiRoute<{
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
await Promise.all( await Promise.all(
objects.map(async status => statusToAPI(status, user)) objects.map(async (status) => statusToAPI(status, user)),
), ),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -72,17 +72,19 @@ export default apiRoute<{
const urlWithoutQuery = req.url.split("?")[0]; const urlWithoutQuery = req.url.split("?")[0];
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
); );
} }
return jsonResponse( return jsonResponse(
await Promise.all( await Promise.all(
objects.map(async status => statusToAPI(status, user || undefined)) objects.map(async (status) =>
statusToAPI(status, user || undefined),
),
), ),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),
} },
); );
}); });

View file

@ -1,11 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource";
import { encode } from "blurhash"; import { encode } from "blurhash";
import sharp from "sharp";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
import type { MediaBackend } from "media-manager"; import type { MediaBackend } from "media-manager";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import sharp from "sharp";
import { client } from "~database/datasource";
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
import { LocalMediaBackend } from "~packages/media-manager/backends/local"; import { LocalMediaBackend } from "~packages/media-manager/backends/local";
import { S3MediaBackend } from "~packages/media-manager/backends/s3"; import { S3MediaBackend } from "~packages/media-manager/backends/s3";
@ -49,7 +49,7 @@ export default apiRoute<{
if (file.size > config.validation.max_media_size) { if (file.size > config.validation.max_media_size) {
return errorResponse( return errorResponse(
`File too large, max size is ${config.validation.max_media_size} bytes`, `File too large, max size is ${config.validation.max_media_size} bytes`,
413 413,
); );
} }
@ -66,7 +66,7 @@ export default apiRoute<{
) { ) {
return errorResponse( return errorResponse(
`Description too long, max length is ${config.validation.max_media_description_size} characters`, `Description too long, max length is ${config.validation.max_media_description_size} characters`,
413 413,
); );
} }
@ -84,7 +84,7 @@ export default apiRoute<{
metadata?.width ?? 0, metadata?.width ?? 0,
metadata?.height ?? 0, metadata?.height ?? 0,
4, 4,
4 4,
) )
: null; : null;
@ -136,13 +136,13 @@ export default apiRoute<{
if (isImage) { if (isImage) {
return jsonResponse(attachmentToAPI(newAttachment)); return jsonResponse(attachmentToAPI(newAttachment));
} else { }
return jsonResponse( return jsonResponse(
{ {
...attachmentToAPI(newAttachment), ...attachmentToAPI(newAttachment),
url: null, url: null,
}, },
202 202,
); );
}
}); });

View file

@ -59,7 +59,7 @@ export default apiRoute<{
if (!user && (resolve || offset)) { if (!user && (resolve || offset)) {
return errorResponse( return errorResponse(
"Cannot use resolve or offset without being authenticated", "Cannot use resolve or offset without being authenticated",
401 401,
); );
} }
@ -97,7 +97,7 @@ export default apiRoute<{
const accounts = await client.user.findMany({ const accounts = await client.user.findMany({
where: { where: {
id: { id: {
in: accountResults.map(hit => hit.id), in: accountResults.map((hit) => hit.id),
}, },
relationshipSubjects: { relationshipSubjects: {
some: { some: {
@ -115,7 +115,7 @@ export default apiRoute<{
const statuses = await client.status.findMany({ const statuses = await client.status.findMany({
where: { where: {
id: { id: {
in: statusResults.map(hit => hit.id), in: statusResults.map((hit) => hit.id),
}, },
author: { author: {
relationshipSubjects: { relationshipSubjects: {
@ -134,9 +134,9 @@ export default apiRoute<{
}); });
return jsonResponse({ return jsonResponse({
accounts: accounts.map(account => userToAPI(account)), accounts: accounts.map((account) => userToAPI(account)),
statuses: await Promise.all( statuses: await Promise.all(
statuses.map(status => statusToAPI(status)) statuses.map((status) => statusToAPI(status)),
), ),
hashtags: [], hashtags: [],
}); });

View file

@ -1,5 +1,5 @@
import { randomBytes } from "node:crypto";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { randomBytes } from "crypto";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { TokenType } from "~database/entities/Token"; import { TokenType } from "~database/entities/Token";
import { userRelations } from "~database/entities/relations"; import { userRelations } from "~database/entities/relations";
@ -34,12 +34,11 @@ export default apiRoute<{
const redirectToLogin = (error: string) => const redirectToLogin = (error: string) =>
Response.redirect( Response.redirect(
`/oauth/authorize?` + `/oauth/authorize?${new URLSearchParams({
new URLSearchParams({
...matchedRoute.query, ...matchedRoute.query,
error: encodeURIComponent(error), error: encodeURIComponent(error),
}).toString(), }).toString()}`,
302 302,
); );
if (response_type !== "code") if (response_type !== "code")
@ -91,15 +90,14 @@ export default apiRoute<{
// Redirect to OAuth confirmation screen // Redirect to OAuth confirmation screen
return Response.redirect( return Response.redirect(
`/oauth/redirect?` + `/oauth/redirect?${new URLSearchParams({
new URLSearchParams({
redirect_uri, redirect_uri,
code, code,
client_id, client_id,
application: application.name, application: application.name,
website: application.website ?? "", website: application.website ?? "",
scope: scopes.join(" "), scope: scopes.join(" "),
}).toString(), }).toString()}`,
302 302,
); );
}); });

View file

@ -27,12 +27,11 @@ export default apiRoute<{
const redirectToLogin = (error: string) => const redirectToLogin = (error: string) =>
Response.redirect( Response.redirect(
`/oauth/authorize?` + `/oauth/authorize?${new URLSearchParams({
new URLSearchParams({
...matchedRoute.query, ...matchedRoute.query,
error: encodeURIComponent(error), error: encodeURIComponent(error),
}).toString(), }).toString()}`,
302 302,
); );
// Get token // Get token

View file

@ -1,5 +1,5 @@
import { errorResponse } from "@response";
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse } from "@response";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -19,7 +19,7 @@ export default apiRoute(async (req, matchedRoute) => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
// parse `Range` header // parse `Range` header
const [start = 0, end = Infinity] = ( const [start = 0, end = Number.POSITIVE_INFINITY] = (
(req.headers.get("Range") || "") (req.headers.get("Range") || "")
.split("=") // ["Range: bytes", "0-100"] .split("=") // ["Range: bytes", "0-100"]
.at(-1) || "" .at(-1) || ""

Some files were not shown because too many files have changed in this diff Show more