mirror of
https://github.com/versia-pub/server.git
synced 2026-03-12 21:39:15 +01:00
Replace eslint and prettier with Biome
This commit is contained in:
parent
4a5a2ea590
commit
af0d627f19
199 changed files with 16493 additions and 16361 deletions
|
|
@ -17,6 +17,6 @@ module.exports = {
|
|||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/consistent-type-exports": "error",
|
||||
"@typescript-eslint/consistent-type-imports": "error"
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
|
|
@ -4,10 +4,7 @@
|
|||
"type": "node",
|
||||
"name": "vscode-jest-tests.v2.lysand",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"test",
|
||||
"${jest.testFile}"
|
||||
],
|
||||
"args": ["test", "${jest.testFile}"],
|
||||
"cwd": "/home/jessew/Dev/lysand",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ const requests: Promise<Response>[] = [];
|
|||
// Repeat 1000 times
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
requests.push(
|
||||
fetch(`https://mastodon.social`, {
|
||||
fetch("https://mastodon.social", {
|
||||
method: "GET",
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ const requestCount = Number(process.argv[2]) || 100;
|
|||
if (!token) {
|
||||
console.log(
|
||||
`${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);
|
||||
}
|
||||
|
|
@ -22,33 +22,33 @@ const fetchTimeline = () =>
|
|||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).then(res => res.ok);
|
||||
}).then((res) => res.ok);
|
||||
|
||||
const timeNow = performance.now();
|
||||
|
||||
const requests = Array.from({ length: requestCount }, () => fetchTimeline());
|
||||
|
||||
Promise.all(requests)
|
||||
.then(results => {
|
||||
.then((results) => {
|
||||
const timeTaken = performance.now() - timeNow;
|
||||
if (results.every(t => t)) {
|
||||
if (results.every((t) => t)) {
|
||||
console.log(`${chalk.green("✓")} All requests succeeded`);
|
||||
} else {
|
||||
console.log(
|
||||
`${chalk.red("✗")} ${
|
||||
results.filter(t => !t).length
|
||||
} requests failed`
|
||||
results.filter((t) => !t).length
|
||||
} requests failed`,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`${chalk.green("✓")} ${
|
||||
requests.length
|
||||
} requests fulfilled in ${chalk.bold(
|
||||
(timeTaken / 1000).toFixed(5)
|
||||
)}s`
|
||||
(timeTaken / 1000).toFixed(5),
|
||||
)}s`,
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.log(`${chalk.red("✗")} ${err}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"ignore": ["node_modules/**/*", "dist/**/*"]
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"ignore": ["node_modules/**/*", "dist/**/*"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4
|
||||
"indentWidth": 4,
|
||||
"ignore": ["node_modules/**/*", "dist/**/*"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
build.ts
24
build.ts
|
|
@ -1,5 +1,5 @@
|
|||
// Delete dist directory
|
||||
import { rm, cp, mkdir, exists } from "fs/promises";
|
||||
import { cp, exists, mkdir, rm } from "node:fs/promises";
|
||||
import { rawRoutes } from "~routes";
|
||||
|
||||
if (!(await exists("./pages/dist"))) {
|
||||
|
|
@ -11,23 +11,23 @@ console.log(`Building at ${process.cwd()}`);
|
|||
|
||||
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
|
||||
await Bun.build({
|
||||
entrypoints: [
|
||||
process.cwd() + "/index.ts",
|
||||
process.cwd() + "/prisma.ts",
|
||||
process.cwd() + "/cli.ts",
|
||||
`${process.cwd()}/index.ts`,
|
||||
`${process.cwd()}/prisma.ts`,
|
||||
`${process.cwd()}/cli.ts`,
|
||||
// Force Bun to include endpoints
|
||||
...Object.values(rawRoutes),
|
||||
],
|
||||
outdir: process.cwd() + "/dist",
|
||||
outdir: `${process.cwd()}/dist`,
|
||||
target: "bun",
|
||||
splitting: true,
|
||||
minify: true,
|
||||
external: ["bullmq"],
|
||||
}).then(output => {
|
||||
}).then((output) => {
|
||||
if (!output.success) {
|
||||
console.log(output.logs);
|
||||
}
|
||||
|
|
@ -35,18 +35,18 @@ await Bun.build({
|
|||
|
||||
// Create pages directory
|
||||
// mkdir ./dist/pages
|
||||
await mkdir(process.cwd() + "/dist/pages");
|
||||
await mkdir(`${process.cwd()}/dist/pages`);
|
||||
|
||||
// Copy Vite build output to dist
|
||||
// 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,
|
||||
});
|
||||
|
||||
// Copy the Bee Movie script from pages
|
||||
await cp(
|
||||
process.cwd() + "/pages/beemovie.txt",
|
||||
process.cwd() + "/dist/pages/beemovie.txt"
|
||||
`${process.cwd()}/pages/beemovie.txt`,
|
||||
`${process.cwd()}/dist/pages/beemovie.txt`,
|
||||
);
|
||||
|
||||
console.log(`Built!`);
|
||||
console.log("Built!");
|
||||
|
|
|
|||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { APIApplication } from "~types/entities/application";
|
||||
import type { Application } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIApplication } from "~types/entities/application";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const getFromToken = async (
|
||||
token: string
|
||||
token: string,
|
||||
): Promise<Application | null> => {
|
||||
const dbToken = await client.token.findFirst({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import type { Attachment } from "@prisma/client";
|
||||
import type { ConfigType } from "config-manager";
|
||||
import type { Config } from "config-manager";
|
||||
import { MediaBackendType } from "media-manager";
|
||||
import type { APIAsyncAttachment } from "~types/entities/async_attachment";
|
||||
import type { APIAttachment } from "~types/entities/attachment";
|
||||
|
||||
export const attachmentToAPI = (
|
||||
attachment: Attachment
|
||||
attachment: Attachment,
|
||||
): APIAsyncAttachment | APIAttachment => {
|
||||
let type = "unknown";
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ export const attachmentToAPI = (
|
|||
|
||||
return {
|
||||
id: attachment.id,
|
||||
type: type as any,
|
||||
type: type as "image" | "video" | "audio" | "unknown",
|
||||
url: attachment.url,
|
||||
remote_url: attachment.remote_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
|
||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||
return `${config.http.base_url}/media/${name}`;
|
||||
// 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 "";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Emoji } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIEmoji } from "~types/entities/emoji";
|
||||
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.
|
||||
|
|
@ -19,7 +19,7 @@ export const parseEmojis = async (text: string): Promise<Emoji[]> => {
|
|||
return await client.emoji.findMany({
|
||||
where: {
|
||||
shortcode: {
|
||||
in: matches.map(match => match.replace(/:/g, "")),
|
||||
in: matches.map((match) => match.replace(/:/g, "")),
|
||||
},
|
||||
instanceId: null,
|
||||
},
|
||||
|
|
@ -81,7 +81,7 @@ export const emojiToLysand = (emoji: Emoji): LysandEmoji => {
|
|||
* Converts the emoji to an 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
|
||||
return {
|
||||
type: "Emoji",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
export const addInstanceIfNotExists = async (
|
||||
url: string
|
||||
url: string,
|
||||
): Promise<Instance> => {
|
||||
const origin = new URL(url).origin;
|
||||
const hostname = new URL(url).hostname;
|
||||
|
|
@ -26,8 +26,8 @@ export const addInstanceIfNotExists = async (
|
|||
if (found) return found;
|
||||
|
||||
// Fetch the instance configuration
|
||||
const metadata = (await fetch(`${origin}/.well-known/lysand`).then(res =>
|
||||
res.json()
|
||||
const metadata = (await fetch(`${origin}/.well-known/lysand`).then((res) =>
|
||||
res.json(),
|
||||
)) as Partial<ServerMetadata>;
|
||||
|
||||
if (metadata.type !== "ServerMetadata") {
|
||||
|
|
@ -43,7 +43,7 @@ export const addInstanceIfNotExists = async (
|
|||
base_url: hostname,
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
logo: metadata.logo as any,
|
||||
logo: metadata.logo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
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 { config } from "config-manager";
|
||||
import type { UserWithRelations } from "./User";
|
||||
|
||||
/**
|
||||
* Represents a Like entity in the database.
|
||||
|
|
@ -12,9 +12,11 @@ import { config } from "config-manager";
|
|||
export const toLysand = (like: Like): LysandLike => {
|
||||
return {
|
||||
id: like.id,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||
author: (like as any).liker?.uri,
|
||||
type: "Like",
|
||||
created_at: new Date(like.createdAt).toISOString(),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||
object: (like as any).liked?.uri,
|
||||
uri: `${config.http.base_url}/actions/${like.id}`,
|
||||
};
|
||||
|
|
@ -27,7 +29,7 @@ export const toLysand = (like: Like): LysandLike => {
|
|||
*/
|
||||
export const createLike = async (
|
||||
user: UserWithRelations,
|
||||
status: StatusWithRelations
|
||||
status: StatusWithRelations,
|
||||
) => {
|
||||
await client.like.create({
|
||||
data: {
|
||||
|
|
@ -58,7 +60,7 @@ export const createLike = async (
|
|||
*/
|
||||
export const deleteLike = async (
|
||||
user: UserWithRelations,
|
||||
status: StatusWithRelations
|
||||
status: StatusWithRelations,
|
||||
) => {
|
||||
await client.like.deleteMany({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export type NotificationWithRelations = Notification & {
|
|||
};
|
||||
|
||||
export const notificationToAPI = async (
|
||||
notification: NotificationWithRelations
|
||||
notification: NotificationWithRelations,
|
||||
): Promise<APINotification> => {
|
||||
return {
|
||||
account: userToAPI(notification.account),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const createFromObject = async (object: LysandObjectType) => {
|
|||
}
|
||||
|
||||
const author = await client.lysandObject.findFirst({
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
where: { uri: (object as any).author },
|
||||
});
|
||||
|
||||
|
|
@ -43,8 +44,8 @@ export const createFromObject = async (object: LysandObjectType) => {
|
|||
"extensions",
|
||||
"type",
|
||||
"uri",
|
||||
].includes(key)
|
||||
)
|
||||
].includes(key),
|
||||
),
|
||||
),
|
||||
},
|
||||
});
|
||||
|
|
@ -56,7 +57,6 @@ export const toLysand = (lyObject: LysandObject): LysandObjectType => {
|
|||
created_at: new Date(lyObject.created_at).toISOString(),
|
||||
type: lyObject.type,
|
||||
uri: lyObject.uri,
|
||||
// @ts-expect-error This works, I promise
|
||||
...lyObject.extra_data,
|
||||
extensions: lyObject.extensions,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// import { Worker } from "bullmq";
|
||||
import { statusToLysand, type StatusWithRelations } from "./Status";
|
||||
import type { User } from "@prisma/client";
|
||||
import { config } from "config-manager";
|
||||
// import { Worker } from "bullmq";
|
||||
import { type StatusWithRelations, statusToLysand } from "./Status";
|
||||
|
||||
/* export const federationWorker = new Worker(
|
||||
"federation",
|
||||
|
|
@ -134,19 +134,19 @@ export const str2ab = (str: string) => {
|
|||
export const federateStatusTo = async (
|
||||
status: StatusWithRelations,
|
||||
sender: User,
|
||||
user: User
|
||||
user: User,
|
||||
) => {
|
||||
const privateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
str2ab(atob(user.privateKey ?? "")),
|
||||
"Ed25519",
|
||||
false,
|
||||
["sign"]
|
||||
["sign"],
|
||||
);
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode("request_body")
|
||||
new TextEncoder().encode("request_body"),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
|
|
@ -162,13 +162,13 @@ export const federateStatusTo = async (
|
|||
`host: ${userInbox.host}\n` +
|
||||
`date: ${date.toUTCString()}\n` +
|
||||
`digest: SHA-256=${btoa(
|
||||
String.fromCharCode(...new Uint8Array(digest))
|
||||
)}\n`
|
||||
)
|
||||
String.fromCharCode(...new Uint8Array(digest)),
|
||||
)}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const signatureBase64 = btoa(
|
||||
String.fromCharCode(...new Uint8Array(signature))
|
||||
String.fromCharCode(...new Uint8Array(signature)),
|
||||
);
|
||||
|
||||
return fetch(userInbox, {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Relationship, User } from "@prisma/client";
|
||||
import type { APIRelationship } from "~types/entities/relationship";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIRelationship } from "~types/entities/relationship";
|
||||
|
||||
/**
|
||||
* Stores Mastodon API relationships
|
||||
|
|
@ -14,7 +14,7 @@ import { client } from "~database/datasource";
|
|||
*/
|
||||
export const createNewRelationship = async (
|
||||
owner: User,
|
||||
other: User
|
||||
other: User,
|
||||
): Promise<Relationship> => {
|
||||
return await client.relationship.create({
|
||||
data: {
|
||||
|
|
@ -40,7 +40,7 @@ export const createNewRelationship = async (
|
|||
export const checkForBidirectionalRelationships = async (
|
||||
user1: User,
|
||||
user2: User,
|
||||
createIfNotExists = true
|
||||
createIfNotExists = true,
|
||||
): Promise<boolean> => {
|
||||
const relationship1 = await client.relationship.findFirst({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -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 { addStausToMeilisearch } from "@meilisearch";
|
||||
import {
|
||||
Prisma,
|
||||
type Application,
|
||||
type Emoji,
|
||||
Prisma,
|
||||
type Relationship,
|
||||
type Status,
|
||||
type User,
|
||||
} 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 { LysandPublication, Note } from "~types/lysand/Object";
|
||||
import { applicationToAPI } from "./Application";
|
||||
import { attachmentToAPI } from "./Attachment";
|
||||
import type { APIAttachment } from "~types/entities/attachment";
|
||||
import { sanitizeHtml } from "@sanitization";
|
||||
import { parse } from "marked";
|
||||
import linkifyStr from "linkify-string";
|
||||
import linkifyHtml from "linkify-html";
|
||||
import { addStausToMeilisearch } from "@meilisearch";
|
||||
import { config } from "config-manager";
|
||||
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import type { UserWithRelations } from "./User";
|
||||
import { fetchRemoteUser, parseMentionsUris, userToAPI } from "./User";
|
||||
import { statusAndUserRelations, userRelations } from "./relations";
|
||||
|
||||
const statusRelations = Prisma.validator<Prisma.StatusDefaultArgs>()({
|
||||
|
|
@ -46,16 +46,15 @@ export type StatusWithRelations = Prisma.StatusGetPayload<
|
|||
export const isViewableByUser = (status: Status, user: User | null) => {
|
||||
if (status.authorId === user?.id) return true;
|
||||
if (status.visibility === "public") return true;
|
||||
else if (status.visibility === "unlisted") return true;
|
||||
else if (status.visibility === "private") {
|
||||
if (status.visibility === "unlisted") return true;
|
||||
if (status.visibility === "private") {
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
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
|
||||
return user && (status.mentions as User[]).includes(user);
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
|
||||
|
|
@ -109,7 +108,7 @@ export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
|
|||
reply: replyStatus
|
||||
? {
|
||||
status: replyStatus,
|
||||
user: (replyStatus as any).author,
|
||||
user: (replyStatus as StatusWithRelations).author,
|
||||
}
|
||||
: 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
|
||||
export const getAncestors = async (
|
||||
status: StatusWithRelations,
|
||||
fetcher: UserWithRelations | null
|
||||
fetcher: UserWithRelations | null,
|
||||
) => {
|
||||
const ancestors: StatusWithRelations[] = [];
|
||||
|
||||
|
|
@ -145,8 +144,8 @@ export const getAncestors = async (
|
|||
|
||||
// Filter for posts that are viewable by the user
|
||||
|
||||
const viewableAncestors = ancestors.filter(ancestor =>
|
||||
isViewableByUser(ancestor, fetcher)
|
||||
const viewableAncestors = ancestors.filter((ancestor) =>
|
||||
isViewableByUser(ancestor, fetcher),
|
||||
);
|
||||
return viewableAncestors;
|
||||
};
|
||||
|
|
@ -159,7 +158,7 @@ export const getAncestors = async (
|
|||
export const getDescendants = async (
|
||||
status: StatusWithRelations,
|
||||
fetcher: UserWithRelations | null,
|
||||
depth = 0
|
||||
depth = 0,
|
||||
) => {
|
||||
const descendants: StatusWithRelations[] = [];
|
||||
|
||||
|
|
@ -181,7 +180,7 @@ export const getDescendants = async (
|
|||
const childDescendants = await getDescendants(
|
||||
child,
|
||||
fetcher,
|
||||
depth + 1
|
||||
depth + 1,
|
||||
);
|
||||
descendants.push(...childDescendants);
|
||||
}
|
||||
|
|
@ -189,8 +188,8 @@ export const getDescendants = async (
|
|||
|
||||
// Filter for posts that are viewable by the user
|
||||
|
||||
const viewableDescendants = descendants.filter(descendant =>
|
||||
isViewableByUser(descendant, fetcher)
|
||||
const viewableDescendants = descendants.filter((descendant) =>
|
||||
isViewableByUser(descendant, fetcher),
|
||||
);
|
||||
return viewableDescendants;
|
||||
};
|
||||
|
|
@ -233,7 +232,7 @@ export const createNewStatus = async (data: {
|
|||
if (mentions.length === 0) {
|
||||
mentions = await client.user.findMany({
|
||||
where: {
|
||||
OR: mentionedPeople.map(person => ({
|
||||
OR: mentionedPeople.map((person) => ({
|
||||
username: person.split("@")[1],
|
||||
instance: {
|
||||
base_url: person.split("@")[2],
|
||||
|
|
@ -244,12 +243,12 @@ export const createNewStatus = async (data: {
|
|||
});
|
||||
}
|
||||
|
||||
let formattedContent;
|
||||
let formattedContent = "";
|
||||
|
||||
// Get HTML version of content
|
||||
if (data.content_type === "text/markdown") {
|
||||
formattedContent = linkifyHtml(
|
||||
await sanitizeHtml(await parse(data.content))
|
||||
await sanitizeHtml(await parse(data.content)),
|
||||
);
|
||||
} else if (data.content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
|
|
@ -260,7 +259,7 @@ export const createNewStatus = async (data: {
|
|||
// Split by newline and add <p> tags
|
||||
formattedContent = formattedContent
|
||||
.split("\n")
|
||||
.map(line => `<p>${line}</p>`)
|
||||
.map((line) => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
|
|
@ -275,7 +274,7 @@ export const createNewStatus = async (data: {
|
|||
sensitive: data.sensitive,
|
||||
spoilerText: data.spoiler_text,
|
||||
emojis: {
|
||||
connect: data.emojis.map(emoji => {
|
||||
connect: data.emojis.map((emoji) => {
|
||||
return {
|
||||
id: emoji.id,
|
||||
};
|
||||
|
|
@ -283,7 +282,7 @@ export const createNewStatus = async (data: {
|
|||
},
|
||||
attachments: data.media_attachments
|
||||
? {
|
||||
connect: data.media_attachments.map(attachment => {
|
||||
connect: data.media_attachments.map((attachment) => {
|
||||
return {
|
||||
id: attachment,
|
||||
};
|
||||
|
|
@ -298,7 +297,7 @@ export const createNewStatus = async (data: {
|
|||
data.uri ||
|
||||
`${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`,
|
||||
mentions: {
|
||||
connect: mentions.map(mention => {
|
||||
connect: mentions.map((mention) => {
|
||||
return {
|
||||
id: mention.id,
|
||||
};
|
||||
|
|
@ -349,7 +348,7 @@ export const editStatus = async (
|
|||
uri?: string;
|
||||
mentions?: User[];
|
||||
media_attachments?: string[];
|
||||
}
|
||||
},
|
||||
) => {
|
||||
// Get people mentioned in the content (match @username or @username@domain.com mentions
|
||||
const mentionedPeople =
|
||||
|
|
@ -366,7 +365,7 @@ export const editStatus = async (
|
|||
if (mentions.length === 0) {
|
||||
mentions = await client.user.findMany({
|
||||
where: {
|
||||
OR: mentionedPeople.map(person => ({
|
||||
OR: mentionedPeople.map((person) => ({
|
||||
username: person.split("@")[1],
|
||||
instance: {
|
||||
base_url: person.split("@")[2],
|
||||
|
|
@ -377,12 +376,12 @@ export const editStatus = async (
|
|||
});
|
||||
}
|
||||
|
||||
let formattedContent;
|
||||
let formattedContent = "";
|
||||
|
||||
// Get HTML version of content
|
||||
if (data.content_type === "text/markdown") {
|
||||
formattedContent = linkifyHtml(
|
||||
await sanitizeHtml(await parse(data.content))
|
||||
await sanitizeHtml(await parse(data.content)),
|
||||
);
|
||||
} else if (data.content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
|
|
@ -393,7 +392,7 @@ export const editStatus = async (
|
|||
// Split by newline and add <p> tags
|
||||
formattedContent = formattedContent
|
||||
.split("\n")
|
||||
.map(line => `<p>${line}</p>`)
|
||||
.map((line) => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
|
|
@ -409,7 +408,7 @@ export const editStatus = async (
|
|||
sensitive: data.sensitive,
|
||||
spoilerText: data.spoiler_text,
|
||||
emojis: {
|
||||
connect: data.emojis.map(emoji => {
|
||||
connect: data.emojis.map((emoji) => {
|
||||
return {
|
||||
id: emoji.id,
|
||||
};
|
||||
|
|
@ -417,7 +416,7 @@ export const editStatus = async (
|
|||
},
|
||||
attachments: data.media_attachments
|
||||
? {
|
||||
connect: data.media_attachments.map(attachment => {
|
||||
connect: data.media_attachments.map((attachment) => {
|
||||
return {
|
||||
id: attachment,
|
||||
};
|
||||
|
|
@ -425,7 +424,7 @@ export const editStatus = async (
|
|||
}
|
||||
: undefined,
|
||||
mentions: {
|
||||
connect: mentions.map(mention => {
|
||||
connect: mentions.map((mention) => {
|
||||
return {
|
||||
id: mention.id,
|
||||
};
|
||||
|
|
@ -453,7 +452,7 @@ export const isFavouritedBy = async (status: Status, user: User) => {
|
|||
*/
|
||||
export const statusToAPI = async (
|
||||
status: StatusWithRelations,
|
||||
user?: UserWithRelations
|
||||
user?: UserWithRelations,
|
||||
): Promise<APIStatus> => {
|
||||
return {
|
||||
id: status.id,
|
||||
|
|
@ -467,25 +466,25 @@ export const statusToAPI = async (
|
|||
: null,
|
||||
card: null,
|
||||
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
|
||||
favourited: !!(status.likes ?? []).find(
|
||||
like => like.likerId === user?.id
|
||||
(like) => like.likerId === user?.id,
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
favourites_count: (status.likes ?? []).length,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
media_attachments: (status.attachments ?? []).map(
|
||||
a => attachmentToAPI(a) as APIAttachment
|
||||
(a) => attachmentToAPI(a) as APIAttachment,
|
||||
),
|
||||
// @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,
|
||||
muted: user
|
||||
? user.relationships.find(r => r.subjectId == status.authorId)
|
||||
? user.relationships.find((r) => r.subjectId === status.authorId)
|
||||
?.muting || 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
|
||||
poll: null,
|
||||
reblog: status.reblog
|
||||
|
|
@ -508,7 +507,7 @@ export const statusToAPI = async (
|
|||
bookmarked: false,
|
||||
quote: status.quotingPost
|
||||
? await statusToAPI(
|
||||
status.quotingPost as unknown as StatusWithRelations
|
||||
status.quotingPost as unknown as StatusWithRelations,
|
||||
)
|
||||
: null,
|
||||
quote_id: status.quotingPost?.id || undefined,
|
||||
|
|
@ -583,13 +582,13 @@ export const statusToLysand = (status: StatusWithRelations): Note => {
|
|||
// TODO: Add attachments
|
||||
attachments: [],
|
||||
is_sensitive: status.sensitive,
|
||||
mentions: status.mentions.map(mention => mention.uri),
|
||||
mentions: status.mentions.map((mention) => mention.uri),
|
||||
quotes: status.quotingPost ? [status.quotingPost.uri] : [],
|
||||
replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [],
|
||||
subject: status.spoilerText,
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
emojis: status.emojis.map(emoji => emojiToLysand(emoji)),
|
||||
emojis: status.emojis.map((emoji) => emojiToLysand(emoji)),
|
||||
},
|
||||
// TODO: Add polls and reactions
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import type { APIAccount } from "~types/entities/account";
|
||||
import type { LysandUser } from "~types/lysand/Object";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import { addUserToMeilisearch } from "@meilisearch";
|
||||
import type { User } 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 { 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 { 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 { MediaBackendType } from "~packages/media-manager";
|
||||
|
||||
export interface AuthData {
|
||||
user: UserWithRelations | null;
|
||||
|
|
@ -38,7 +38,8 @@ export const getAvatarUrl = (user: User, config: Config) => {
|
|||
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||
return `${config.http.base_url}/media/${user.avatar}`;
|
||||
// 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 "";
|
||||
|
|
@ -54,7 +55,8 @@ export const getHeaderUrl = (user: User, config: Config) => {
|
|||
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||
return `${config.http.base_url}/media/${user.header}`;
|
||||
// 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 "";
|
||||
|
|
@ -125,8 +127,8 @@ export const fetchRemoteUser = async (uri: string) => {
|
|||
inbox: data.inbox,
|
||||
outbox: data.outbox,
|
||||
},
|
||||
avatar: (data.avatar && data.avatar[0].content) || "",
|
||||
header: (data.header && data.header[0].content) || "",
|
||||
avatar: data.avatar?.[0].content || "",
|
||||
header: data.header?.[0].content || "",
|
||||
displayName: data.display_name ?? "",
|
||||
note: data.bio?.[0].content ?? "",
|
||||
publicKey: data.public_key.public_key,
|
||||
|
|
@ -157,7 +159,7 @@ export const fetchRemoteUser = async (uri: string) => {
|
|||
},
|
||||
data: {
|
||||
emojis: {
|
||||
connect: emojis.map(emoji => ({
|
||||
connect: emojis.map((emoji) => ({
|
||||
id: emoji.id,
|
||||
})),
|
||||
},
|
||||
|
|
@ -282,7 +284,7 @@ export const retrieveUserFromToken = async (access_token: string) => {
|
|||
*/
|
||||
export const getRelationshipToOtherUser = async (
|
||||
user: UserWithRelations,
|
||||
other: User
|
||||
other: User,
|
||||
) => {
|
||||
return await client.relationship.findFirst({
|
||||
where: {
|
||||
|
|
@ -305,17 +307,17 @@ export const generateUserKeys = async () => {
|
|||
String.fromCharCode.apply(null, [
|
||||
...new Uint8Array(
|
||||
// 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(
|
||||
String.fromCharCode(
|
||||
...new Uint8Array(
|
||||
// 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
|
||||
|
|
@ -328,7 +330,7 @@ export const generateUserKeys = async () => {
|
|||
|
||||
export const userToAPI = (
|
||||
user: UserWithRelations,
|
||||
isOwnAccount = false
|
||||
isOwnAccount = false,
|
||||
): APIAccount => {
|
||||
return {
|
||||
id: user.id,
|
||||
|
|
@ -340,11 +342,11 @@ export const userToAPI = (
|
|||
header: getHeaderUrl(user, config),
|
||||
locked: user.isLocked,
|
||||
created_at: new Date(user.createdAt).toISOString(),
|
||||
followers_count: user.relationshipSubjects.filter(r => r.following)
|
||||
followers_count: user.relationshipSubjects.filter((r) => r.following)
|
||||
.length,
|
||||
following_count: user.relationships.filter(r => r.following).length,
|
||||
following_count: user.relationships.filter((r) => r.following).length,
|
||||
statuses_count: user._count.statuses,
|
||||
emojis: user.emojis.map(emoji => emojiToAPI(emoji)),
|
||||
emojis: user.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||
// TODO: Add fields
|
||||
fields: [],
|
||||
bot: user.isBot,
|
||||
|
|
@ -419,7 +421,7 @@ export const userToLysand = (user: UserWithRelations): LysandUser => {
|
|||
},
|
||||
],
|
||||
display_name: user.displayName,
|
||||
fields: (user.source as any as APISource).fields.map(field => ({
|
||||
fields: (user.source as APISource).fields.map((field) => ({
|
||||
key: [
|
||||
{
|
||||
content: field.name,
|
||||
|
|
@ -447,7 +449,7 @@ export const userToLysand = (user: UserWithRelations): LysandUser => {
|
|||
},
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
emojis: user.emojis.map(emoji => emojiToLysand(emoji)),
|
||||
emojis: user.emojis.map((emoji) => emojiToLysand(emoji)),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
24
index.ts
24
index.ts
|
|
@ -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 { initializeRedisCache } from "@redis";
|
||||
import { connectMeili } from "@meilisearch";
|
||||
import { config } from "config-manager";
|
||||
import { client } from "~database/datasource";
|
||||
import { LogLevel, LogManager, MultiLogManager } from "log-manager";
|
||||
import { moduleIsEntry } from "@module";
|
||||
import { client } from "~database/datasource";
|
||||
import { createServer } from "~server";
|
||||
import { exists, mkdir } from "fs/promises";
|
||||
|
||||
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);
|
||||
// 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(
|
||||
isEntry ? Bun.stdout : Bun.file(`/dev/null`)
|
||||
isEntry ? Bun.stdout : Bun.file("/dev/null"),
|
||||
);
|
||||
const dualLogger = new MultiLogManager([logger, consoleLogger]);
|
||||
|
||||
|
|
@ -23,10 +23,10 @@ if (!(await exists(config.logging.storage.requests))) {
|
|||
await consoleLogger.log(
|
||||
LogLevel.WARNING,
|
||||
"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...");
|
||||
|
|
@ -61,13 +61,15 @@ const server = createServer(config, dualLogger, isProd);
|
|||
await dualLogger.log(
|
||||
LogLevel.INFO,
|
||||
"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(
|
||||
LogLevel.INFO,
|
||||
"Database",
|
||||
`Database is online, now serving ${postCount} posts`
|
||||
`Database is online, now serving ${postCount} posts`,
|
||||
);
|
||||
|
||||
export { config, server };
|
||||
|
|
|
|||
14
package.json
14
package.json
|
|
@ -14,11 +14,7 @@
|
|||
},
|
||||
"icon": "https://github.com/lysand-org/lysand",
|
||||
"license": "AGPL-3.0",
|
||||
"keywords": [
|
||||
"federated",
|
||||
"activitypub",
|
||||
"bun"
|
||||
],
|
||||
"keywords": ["federated", "activitypub", "bun"],
|
||||
"workspaces": ["packages/*"],
|
||||
"maintainers": [
|
||||
{
|
||||
|
|
@ -58,24 +54,16 @@
|
|||
"devDependencies": {
|
||||
"@biomejs/biome": "1.6.4",
|
||||
"@julr/unocss-preset-forms": "^0.1.0",
|
||||
"@microsoft/eslint-formatter-sarif": "^3.0.0",
|
||||
"@types/cli-table": "^0.3.4",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/ioredis": "^5.0.0",
|
||||
"@types/jsonld": "^1.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "latest",
|
||||
"@typescript-eslint/parser": "latest",
|
||||
"@unocss/cli": "latest",
|
||||
"@vitejs/plugin-vue": "latest",
|
||||
"@vueuse/head": "^2.0.0",
|
||||
"activitypub-types": "^1.0.3",
|
||||
"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",
|
||||
"unocss": "latest",
|
||||
"untyped": "^1.4.2",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { CliParameterType, type CliParameter } from "./cli-builder.type";
|
||||
import chalk from "chalk";
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -30,7 +30,9 @@ export class CliBuilder {
|
|||
registerCommand(command: CliCommand) {
|
||||
if (this.checkIfCommandAlreadyExists(command)) {
|
||||
throw new Error(
|
||||
`Command category '${command.categories.join(" ")}' already exists`
|
||||
`Command category '${command.categories.join(
|
||||
" ",
|
||||
)}' already exists`,
|
||||
);
|
||||
}
|
||||
this.commands.push(command);
|
||||
|
|
@ -42,12 +44,14 @@ export class CliBuilder {
|
|||
* @param commands Commands to add
|
||||
*/
|
||||
registerCommands(commands: CliCommand[]) {
|
||||
const existingCommand = commands.find(command =>
|
||||
this.checkIfCommandAlreadyExists(command)
|
||||
const existingCommand = commands.find((command) =>
|
||||
this.checkIfCommandAlreadyExists(command),
|
||||
);
|
||||
if (existingCommand) {
|
||||
throw new Error(
|
||||
`Command category '${existingCommand.categories.join(" ")}' already exists`
|
||||
`Command category '${existingCommand.categories.join(
|
||||
" ",
|
||||
)}' already exists`,
|
||||
);
|
||||
}
|
||||
this.commands.push(...commands);
|
||||
|
|
@ -59,7 +63,7 @@ export class CliBuilder {
|
|||
*/
|
||||
deregisterCommand(command: CliCommand) {
|
||||
this.commands = this.commands.filter(
|
||||
registeredCommand => registeredCommand !== command
|
||||
(registeredCommand) => registeredCommand !== command,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -69,18 +73,18 @@ export class CliBuilder {
|
|||
*/
|
||||
deregisterCommands(commands: CliCommand[]) {
|
||||
this.commands = this.commands.filter(
|
||||
registeredCommand => !commands.includes(registeredCommand)
|
||||
(registeredCommand) => !commands.includes(registeredCommand),
|
||||
);
|
||||
}
|
||||
|
||||
checkIfCommandAlreadyExists(command: CliCommand) {
|
||||
return this.commands.some(
|
||||
registeredCommand =>
|
||||
registeredCommand.categories.length ==
|
||||
(registeredCommand) =>
|
||||
registeredCommand.categories.length ===
|
||||
command.categories.length &&
|
||||
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("./")) {
|
||||
// Formatted like ./cli.ts [command]
|
||||
return args.slice(1);
|
||||
} else if (args[0].includes("bun")) {
|
||||
}
|
||||
if (args[0].includes("bun")) {
|
||||
// Formatted like bun cli.ts [command]
|
||||
return args.slice(2);
|
||||
} else {
|
||||
return args;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,24 +121,28 @@ export class CliBuilder {
|
|||
|
||||
// Find revelant command
|
||||
// Search for a command with as many categories matching args as possible
|
||||
const matchingCommands = this.commands.filter(command =>
|
||||
startsWithArray(revelantArgs, command.categories)
|
||||
const matchingCommands = this.commands.filter((command) =>
|
||||
startsWithArray(revelantArgs, command.categories),
|
||||
);
|
||||
|
||||
if (matchingCommands.length === 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Get command with largest category size
|
||||
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(
|
||||
command.categories.length
|
||||
command.categories.length,
|
||||
);
|
||||
|
||||
return await command.run(argsWithoutCategories);
|
||||
|
|
@ -210,34 +218,44 @@ export class CliBuilder {
|
|||
const displayTree = (tree: TreeType, depth = 0) => {
|
||||
for (const [key, value] of Object.entries(tree)) {
|
||||
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(
|
||||
arg => arg.positioned ?? true
|
||||
(arg) => arg.positioned ?? true,
|
||||
);
|
||||
const unpositionedArgs = value.argTypes.filter(
|
||||
arg => !(arg.positioned ?? true)
|
||||
(arg) => !(arg.positioned ?? true),
|
||||
);
|
||||
|
||||
for (const arg of positionedArgs) {
|
||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.green(
|
||||
arg.name
|
||||
)}|${
|
||||
writeBuffer += `${" ".repeat(
|
||||
depth + 1,
|
||||
)}${chalk.green(arg.name)}|${
|
||||
arg.description ?? "(no description)"
|
||||
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
|
||||
}
|
||||
for (const arg of unpositionedArgs) {
|
||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}${arg.shortName ? ", " + chalk.yellow("-" + arg.shortName) : ""}|${
|
||||
arg.description ?? "(no description)"
|
||||
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
|
||||
writeBuffer += `${" ".repeat(
|
||||
depth + 1,
|
||||
)}${chalk.yellow(`--${arg.name}`)}${
|
||||
arg.shortName
|
||||
? `, ${chalk.yellow(`-${arg.shortName}`)}`
|
||||
: ""
|
||||
}|${arg.description ?? "(no description)"} ${
|
||||
arg.optional ? chalk.gray("(optional)") : ""
|
||||
}\n`;
|
||||
}
|
||||
|
||||
if (value.example) {
|
||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.bold("Example:")} ${chalk.bgGray(
|
||||
value.example
|
||||
)}\n`;
|
||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.bold(
|
||||
"Example:",
|
||||
)} ${chalk.bgGray(value.example)}\n`;
|
||||
}
|
||||
} else {
|
||||
writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}\n`;
|
||||
writeBuffer += `${" ".repeat(depth)}${chalk.blue(
|
||||
key,
|
||||
)}\n`;
|
||||
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
|
||||
const optimal_length = Number(
|
||||
// @ts-expect-error Slightly hacky but works
|
||||
writeBuffer.split("\n").reduce((prev, current) => {
|
||||
writeBuffer
|
||||
.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 (!prev)
|
||||
return current.includes("|")
|
||||
|
|
@ -257,8 +277,8 @@ export class CliBuilder {
|
|||
if (!current.includes("|")) return prev;
|
||||
const [left] = current.split("|");
|
||||
// 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")) {
|
||||
|
|
@ -268,7 +288,7 @@ export class CliBuilder {
|
|||
continue;
|
||||
}
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -276,7 +296,7 @@ export class CliBuilder {
|
|||
|
||||
type ExecuteFunction<T> = (
|
||||
instance: CliCommand,
|
||||
args: Partial<T>
|
||||
args: Partial<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
) => Promise<number> | Promise<void> | number | void;
|
||||
|
||||
|
|
@ -284,13 +304,15 @@ type ExecuteFunction<T> = (
|
|||
* A command that can be executed from the command line
|
||||
* @param categories Example: `["user", "create"]` for the command `./cli user create --name John`
|
||||
*/
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
export class CliCommand<T = any> {
|
||||
constructor(
|
||||
public categories: string[],
|
||||
public argTypes: CliParameter[],
|
||||
private execute: ExecuteFunction<T>,
|
||||
public description?: string,
|
||||
public example?: string
|
||||
public example?: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -299,10 +321,10 @@ export class CliCommand<T = any> {
|
|||
*/
|
||||
displayHelp() {
|
||||
const positionedArgs = this.argTypes.filter(
|
||||
arg => arg.positioned ?? true
|
||||
(arg) => arg.positioned ?? true,
|
||||
);
|
||||
const unpositionedArgs = this.argTypes.filter(
|
||||
arg => !(arg.positioned ?? true)
|
||||
(arg) => !(arg.positioned ?? true),
|
||||
);
|
||||
const helpMessage = `
|
||||
${chalk.green("📚 Command:")} ${chalk.yellow(this.categories.join(" "))}
|
||||
|
|
@ -310,22 +332,26 @@ ${this.description ? `${chalk.cyan(this.description)}\n` : ""}
|
|||
${chalk.magenta("🔧 Arguments:")}
|
||||
${positionedArgs
|
||||
.map(
|
||||
arg =>
|
||||
`${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${
|
||||
arg.optional ? chalk.gray("(optional)") : ""
|
||||
}`
|
||||
(arg) =>
|
||||
`${chalk.bold(arg.name)}: ${chalk.blue(
|
||||
arg.description ?? "(no description)",
|
||||
)} ${arg.optional ? chalk.gray("(optional)") : ""}`,
|
||||
)
|
||||
.join("\n")}
|
||||
${unpositionedArgs
|
||||
.map(
|
||||
arg =>
|
||||
`--${chalk.bold(arg.name)}${arg.shortName ? `, -${arg.shortName}` : ""}: ${chalk.blue(arg.description ?? "(no description)")} ${
|
||||
(arg) =>
|
||||
`--${chalk.bold(arg.name)}${
|
||||
arg.shortName ? `, -${arg.shortName}` : ""
|
||||
}: ${chalk.blue(arg.description ?? "(no description)")} ${
|
||||
arg.optional ? chalk.gray("(optional)") : ""
|
||||
}`
|
||||
}`,
|
||||
)
|
||||
.join(
|
||||
"\n"
|
||||
)}${this.example ? `\n${chalk.magenta("🚀 Example:")}\n${chalk.bgGray(this.example)}` : ""}
|
||||
.join("\n")}${
|
||||
this.example
|
||||
? `\n${chalk.magenta("🚀 Example:")}\n${chalk.bgGray(this.example)}`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
console.log(helpMessage);
|
||||
|
|
@ -336,8 +362,11 @@ ${unpositionedArgs
|
|||
* @param argsWithoutCategories
|
||||
* @returns
|
||||
*/
|
||||
private parseArgs(argsWithoutCategories: string[]): Record<string, any> {
|
||||
const parsedArgs: Record<string, any> = {};
|
||||
private parseArgs(
|
||||
argsWithoutCategories: string[],
|
||||
): Record<string, string | number | boolean | string[]> {
|
||||
const parsedArgs: Record<string, string | number | boolean | string[]> =
|
||||
{};
|
||||
let currentParameter: CliParameter | null = null;
|
||||
|
||||
for (let i = 0; i < argsWithoutCategories.length; i++) {
|
||||
|
|
@ -346,15 +375,15 @@ ${unpositionedArgs
|
|||
if (arg.startsWith("--")) {
|
||||
const argName = arg.substring(2);
|
||||
currentParameter =
|
||||
this.argTypes.find(argType => argType.name === argName) ||
|
||||
this.argTypes.find((argType) => argType.name === argName) ||
|
||||
null;
|
||||
if (currentParameter && !currentParameter.needsValue) {
|
||||
parsedArgs[argName] = true;
|
||||
currentParameter = null;
|
||||
} else if (currentParameter && currentParameter.needsValue) {
|
||||
} else if (currentParameter?.needsValue) {
|
||||
parsedArgs[argName] = this.castArgValue(
|
||||
argsWithoutCategories[i + 1],
|
||||
currentParameter.type
|
||||
currentParameter.type,
|
||||
);
|
||||
i++;
|
||||
currentParameter = null;
|
||||
|
|
@ -362,31 +391,32 @@ ${unpositionedArgs
|
|||
} else if (arg.startsWith("-")) {
|
||||
const shortName = arg.substring(1);
|
||||
const argType = this.argTypes.find(
|
||||
argType => argType.shortName === shortName
|
||||
(argType) => argType.shortName === shortName,
|
||||
);
|
||||
if (argType && !argType.needsValue) {
|
||||
parsedArgs[argType.name] = true;
|
||||
} else if (argType && argType.needsValue) {
|
||||
} else if (argType?.needsValue) {
|
||||
parsedArgs[argType.name] = this.castArgValue(
|
||||
argsWithoutCategories[i + 1],
|
||||
argType.type
|
||||
argType.type,
|
||||
);
|
||||
i++;
|
||||
}
|
||||
} else if (currentParameter) {
|
||||
parsedArgs[currentParameter.name] = this.castArgValue(
|
||||
arg,
|
||||
currentParameter.type
|
||||
currentParameter.type,
|
||||
);
|
||||
currentParameter = null;
|
||||
} else {
|
||||
const positionedArgType = this.argTypes.find(
|
||||
argType => argType.positioned && !parsedArgs[argType.name]
|
||||
(argType) =>
|
||||
argType.positioned && !parsedArgs[argType.name],
|
||||
);
|
||||
if (positionedArgType) {
|
||||
parsedArgs[positionedArgType.name] = this.castArgValue(
|
||||
arg,
|
||||
positionedArgType.type
|
||||
positionedArgType.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -395,7 +425,10 @@ ${unpositionedArgs
|
|||
return parsedArgs;
|
||||
}
|
||||
|
||||
private castArgValue(value: string, type: CliParameter["type"]): any {
|
||||
private castArgValue(
|
||||
value: string,
|
||||
type: CliParameter["type"],
|
||||
): string | number | boolean | string[] {
|
||||
switch (type) {
|
||||
case CliParameterType.STRING:
|
||||
return value;
|
||||
|
|
@ -415,6 +448,6 @@ ${unpositionedArgs
|
|||
*/
|
||||
async run(argsWithoutCategories: string[]) {
|
||||
const args = this.parseArgs(argsWithoutCategories);
|
||||
return await this.execute(this, args as any);
|
||||
return await this.execute(this, args as T);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// FILEPATH: /home/jessew/Dev/lysand/packages/cli-parser/index.test.ts
|
||||
import { CliCommand, CliBuilder, startsWithArray } from "..";
|
||||
import { describe, beforeEach, it, expect, jest, spyOn } from "bun:test";
|
||||
import { beforeEach, describe, expect, it, jest, spyOn } from "bun:test";
|
||||
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";
|
||||
|
||||
describe("startsWithArray", () => {
|
||||
|
|
@ -19,7 +19,7 @@ describe("startsWithArray", () => {
|
|||
|
||||
it("should return true when startArray is empty", () => {
|
||||
const fullArray = ["a", "b", "c", "d", "e"];
|
||||
const startArray: any[] = [];
|
||||
const startArray: string[] = [];
|
||||
expect(startsWithArray(fullArray, startArray)).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -61,12 +61,13 @@ describe("CliCommand", () => {
|
|||
],
|
||||
() => {
|
||||
// Do nothing
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse string arguments correctly", () => {
|
||||
const args = cliCommand["parseArgs"]([
|
||||
// @ts-expect-error Testing private method
|
||||
const args = cliCommand.parseArgs([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"--arg2",
|
||||
|
|
@ -84,7 +85,8 @@ describe("CliCommand", () => {
|
|||
});
|
||||
|
||||
it("should parse short names for arguments too", () => {
|
||||
const args = cliCommand["parseArgs"]([
|
||||
// @ts-expect-error Testing private method
|
||||
const args = cliCommand.parseArgs([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"-a",
|
||||
|
|
@ -102,14 +104,15 @@ describe("CliCommand", () => {
|
|||
});
|
||||
|
||||
it("should cast argument values correctly", () => {
|
||||
expect(cliCommand["castArgValue"]("42", CliParameterType.NUMBER)).toBe(
|
||||
42
|
||||
// @ts-expect-error Testing private method
|
||||
expect(cliCommand.castArgValue("42", CliParameterType.NUMBER)).toBe(42);
|
||||
// @ts-expect-error Testing private method
|
||||
expect(cliCommand.castArgValue("true", CliParameterType.BOOLEAN)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
cliCommand["castArgValue"]("true", CliParameterType.BOOLEAN)
|
||||
).toBe(true);
|
||||
expect(
|
||||
cliCommand["castArgValue"]("value1,value2", CliParameterType.ARRAY)
|
||||
// @ts-expect-error Testing private method
|
||||
cliCommand.castArgValue("value1,value2", CliParameterType.ARRAY),
|
||||
).toEqual(["value1", "value2"]);
|
||||
});
|
||||
|
||||
|
|
@ -139,7 +142,7 @@ describe("CliCommand", () => {
|
|||
needsValue: true,
|
||||
},
|
||||
],
|
||||
mockExecute
|
||||
mockExecute,
|
||||
);
|
||||
|
||||
await cliCommand.run([
|
||||
|
|
@ -191,7 +194,7 @@ describe("CliCommand", () => {
|
|||
positioned: true,
|
||||
},
|
||||
],
|
||||
mockExecute
|
||||
mockExecute,
|
||||
);
|
||||
|
||||
await cliCommand.run([
|
||||
|
|
@ -255,13 +258,13 @@ describe("CliCommand", () => {
|
|||
// Do nothing
|
||||
},
|
||||
"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();
|
||||
|
||||
const loggedString = consoleLogSpy.mock.calls.map(call =>
|
||||
stripAnsi(call[0])
|
||||
const loggedString = consoleLogSpy.mock.calls.map((call) =>
|
||||
stripAnsi(call[0]),
|
||||
)[0];
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
|
|
@ -274,7 +277,7 @@ describe("CliCommand", () => {
|
|||
expect(loggedString).toContain("--arg4: Argument 4");
|
||||
expect(loggedString).toContain("🚀 Example:");
|
||||
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,
|
||||
},
|
||||
],
|
||||
mockExecute
|
||||
mockExecute,
|
||||
);
|
||||
cliBuilder.registerCommand(mockCommand);
|
||||
await cliBuilder.processArgs([
|
||||
|
|
@ -365,7 +368,7 @@ describe("CliBuilder", () => {
|
|||
mockCommand3 = new CliCommand(
|
||||
["user", "new", "admin"],
|
||||
[],
|
||||
jest.fn()
|
||||
jest.fn(),
|
||||
);
|
||||
mockCommand4 = new CliCommand(["user", "new"], [], jest.fn());
|
||||
mockCommand5 = new CliCommand(["admin", "delete"], [], jest.fn());
|
||||
|
|
@ -450,36 +453,36 @@ describe("CliBuilder", () => {
|
|||
// Do nothing
|
||||
},
|
||||
"I love sussy sauces",
|
||||
"emoji add --url https://site.com/image.png"
|
||||
"emoji add --url https://site.com/image.png",
|
||||
);
|
||||
|
||||
cliBuilder.registerCommand(cliCommand);
|
||||
cliBuilder.displayHelp();
|
||||
|
||||
const loggedString = consoleLogSpy.mock.calls
|
||||
.map(call => stripAnsi(call[0]))
|
||||
.map((call) => stripAnsi(call[0]))
|
||||
.join("\n");
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
|
||||
expect(loggedString).toContain("category1");
|
||||
expect(loggedString).toContain(
|
||||
" category2.................I love sussy sauces"
|
||||
" category2.................I love sussy sauces",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" name..................Name of new item"
|
||||
" name..................Name of new item",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" arg3..................(no description)"
|
||||
" arg3..................(no description)",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" arg4..................(no description)"
|
||||
" arg4..................(no description)",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" --delete-previous.....Also delete the previous item (optional)"
|
||||
" --delete-previous.....Also delete the previous item (optional)",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" Example: emoji add --url https://site.com/image.png"
|
||||
" Example: emoji add --url https://site.com/image.png",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,7 +78,14 @@ export interface Config {
|
|||
|
||||
oidc: {
|
||||
/** @default [] */
|
||||
providers: Record<string, any>[];
|
||||
providers: {
|
||||
name: string;
|
||||
id: string;
|
||||
url: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
icon: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
http: {
|
||||
|
|
@ -91,9 +98,9 @@ export interface Config {
|
|||
/** @default "8080" */
|
||||
bind_port: string;
|
||||
|
||||
banned_ips: any[];
|
||||
banned_ips: string[];
|
||||
|
||||
banned_user_agents: any[];
|
||||
banned_user_agents: string[];
|
||||
|
||||
bait: {
|
||||
/** @default false */
|
||||
|
|
@ -229,7 +236,7 @@ export interface Config {
|
|||
/** @default false */
|
||||
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"] */
|
||||
url_scheme_whitelist: string[];
|
||||
|
|
@ -256,28 +263,28 @@ export interface Config {
|
|||
};
|
||||
|
||||
federation: {
|
||||
blocked: any[];
|
||||
blocked: string[];
|
||||
|
||||
followers_only: any[];
|
||||
followers_only: string[];
|
||||
|
||||
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: {
|
||||
note_content: any[];
|
||||
note_content: string[];
|
||||
|
||||
emoji: any[];
|
||||
emoji: string[];
|
||||
|
||||
username: any[];
|
||||
username: string[];
|
||||
|
||||
displayname: any[];
|
||||
displayname: string[];
|
||||
|
||||
bio: any[];
|
||||
bio: string[];
|
||||
};
|
||||
|
||||
logging: {
|
||||
|
|
@ -387,7 +394,7 @@ export const defaultConfig: Config = {
|
|||
],
|
||||
},
|
||||
oidc: {
|
||||
providers: [[]],
|
||||
providers: [],
|
||||
},
|
||||
http: {
|
||||
base_url: "https://lysand.social",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { watchConfig } from "c12";
|
||||
import { defaultConfig, type Config } from "./config.type";
|
||||
import { type Config, defaultConfig } from "./config.type";
|
||||
|
||||
const { config } = await watchConfig<Config>({
|
||||
configFile: "./config/config.toml",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { appendFile } from "node:fs/promises";
|
||||
import type { BunFile } from "bun";
|
||||
import { appendFile } from "fs/promises";
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = "debug",
|
||||
|
|
@ -16,7 +16,7 @@ export enum LogLevel {
|
|||
export class LogManager {
|
||||
constructor(private output: BunFile) {
|
||||
void this.write(
|
||||
`--- INIT LogManager at ${new Date().toISOString()} ---`
|
||||
`--- INIT LogManager at ${new Date().toISOString()} ---`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -31,16 +31,18 @@ export class LogManager {
|
|||
level: LogLevel,
|
||||
entity: string,
|
||||
message: string,
|
||||
showTimestamp = true
|
||||
showTimestamp = true,
|
||||
) {
|
||||
await this.write(
|
||||
`${showTimestamp ? new Date().toISOString() + " " : ""}[${level.toUpperCase()}] ${entity}: ${message}`
|
||||
`${
|
||||
showTimestamp ? `${new Date().toISOString()} ` : ""
|
||||
}[${level.toUpperCase()}] ${entity}: ${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async write(text: string) {
|
||||
if (this.output == Bun.stdout) {
|
||||
await Bun.write(Bun.stdout, text + "\n");
|
||||
if (this.output === Bun.stdout) {
|
||||
await Bun.write(Bun.stdout, `${text}\n`);
|
||||
} else {
|
||||
if (!(await this.output.exists())) {
|
||||
// Create file if it doesn't exist
|
||||
|
|
@ -48,7 +50,7 @@ export class LogManager {
|
|||
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}`;
|
||||
|
||||
if (logAllDetails) {
|
||||
string += `\n`;
|
||||
string += ` [Headers]\n`;
|
||||
string += "\n";
|
||||
string += " [Headers]\n";
|
||||
// Pretty print headers
|
||||
for (const [key, value] of req.headers.entries()) {
|
||||
string += ` ${key}: ${value}\n`;
|
||||
}
|
||||
|
||||
// Pretty print body
|
||||
string += ` [Body]\n`;
|
||||
string += " [Body]\n";
|
||||
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 stringified = JSON.stringify(json, null, 4)
|
||||
.split("\n")
|
||||
.map(line => ` ${line}`)
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
|
||||
string += `${stringified}\n`;
|
||||
|
|
@ -103,7 +105,9 @@ export class LogManager {
|
|||
if (value.toString().length < 300) {
|
||||
string += ` ${key}: ${value.toString()}\n`;
|
||||
} else {
|
||||
string += ` ${key}: <${value.toString().length} bytes>\n`;
|
||||
string += ` ${key}: <${
|
||||
value.toString().length
|
||||
} bytes>\n`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -132,7 +136,7 @@ export class MultiLogManager {
|
|||
level: LogLevel,
|
||||
entity: string,
|
||||
message: string,
|
||||
showTimestamp = true
|
||||
showTimestamp = true,
|
||||
) {
|
||||
for (const logManager of this.logManagers) {
|
||||
await logManager.log(level, entity, message, showTimestamp);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
describe,
|
||||
it,
|
||||
type Mock,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
jest,
|
||||
mock,
|
||||
type Mock,
|
||||
test,
|
||||
} from "bun:test";
|
||||
import type fs from "node:fs/promises";
|
||||
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", () => {
|
||||
let logManager: LogManager;
|
||||
|
|
@ -30,7 +30,7 @@ describe("LogManager", () => {
|
|||
it("should initialize and write init log", () => {
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
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");
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("[INFO] TestEntity: Test message")
|
||||
expect.stringContaining("[INFO] TestEntity: Test message"),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -47,11 +47,11 @@ describe("LogManager", () => {
|
|||
LogLevel.INFO,
|
||||
"TestEntity",
|
||||
"Test message",
|
||||
false
|
||||
false,
|
||||
);
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
"[INFO] TestEntity: Test message\n"
|
||||
"[INFO] TestEntity: Test message\n",
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -68,18 +68,18 @@ describe("LogManager", () => {
|
|||
|
||||
expect(writeMock).toHaveBeenCalledWith(
|
||||
Bun.stdout,
|
||||
expect.stringContaining("[INFO] TestEntity: Test message")
|
||||
expect.stringContaining("[INFO] TestEntity: Test message"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if output file does not exist", () => {
|
||||
mockAppend.mockImplementationOnce(() => {
|
||||
return Promise.reject(
|
||||
new Error("Output file doesnt exist (and isnt stdout)")
|
||||
new Error("Output file doesnt exist (and isnt stdout)"),
|
||||
);
|
||||
});
|
||||
expect(
|
||||
logManager.log(LogLevel.INFO, "TestEntity", "Test message")
|
||||
logManager.log(LogLevel.INFO, "TestEntity", "Test message"),
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ describe("LogManager", () => {
|
|||
await logManager.logError(LogLevel.ERROR, "TestEntity", error);
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("[ERROR] TestEntity: Test error")
|
||||
expect.stringContaining("[ERROR] TestEntity: Test error"),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ describe("LogManager", () => {
|
|||
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
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(
|
||||
mockOutput.name,
|
||||
expect.stringContaining(expectedLog)
|
||||
expect.stringContaining(expectedLog),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ describe("LogManager", () => {
|
|||
`;
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining(expectedLog)
|
||||
expect.stringContaining(expectedLog),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -167,8 +167,8 @@ describe("LogManager", () => {
|
|||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining(
|
||||
expectedLog.replace("----", expect.any(String))
|
||||
)
|
||||
expectedLog.replace("----", expect.any(String)),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -207,7 +207,7 @@ describe("MultiLogManager", () => {
|
|||
LogLevel.INFO,
|
||||
"TestEntity",
|
||||
"Test message",
|
||||
true
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ describe("MultiLogManager", () => {
|
|||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
LogLevel.ERROR,
|
||||
"TestEntity",
|
||||
error
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +1,47 @@
|
|||
import type { Config } from "config-manager";
|
||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||
import type { ConvertableMediaFormats } from "../media-converter";
|
||||
import { MediaConverter } from "../media-converter";
|
||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||
import type { ConfigType } from "config-manager";
|
||||
|
||||
export class LocalMediaBackend extends MediaBackend {
|
||||
constructor(config: ConfigType) {
|
||||
constructor(config: Config) {
|
||||
super(config, MediaBackendType.LOCAL);
|
||||
}
|
||||
|
||||
public async addFile(file: File) {
|
||||
let convertedFile = file;
|
||||
if (this.shouldConvertImages(this.config)) {
|
||||
const fileExtension = file.name.split(".").pop();
|
||||
const mediaConverter = new MediaConverter(
|
||||
fileExtension as ConvertableMediaFormats,
|
||||
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(
|
||||
`${this.config.media.local_uploads_folder}/${hash}`
|
||||
`${this.config.media.local_uploads_folder}/${hash}`,
|
||||
);
|
||||
|
||||
if (await newFile.exists()) {
|
||||
throw new Error("File already exists");
|
||||
}
|
||||
|
||||
await Bun.write(newFile, file);
|
||||
await Bun.write(newFile, convertedFile);
|
||||
|
||||
return {
|
||||
uploadedFile: file,
|
||||
path: `./uploads/${file.name}`,
|
||||
uploadedFile: convertedFile,
|
||||
path: `./uploads/${convertedFile.name}`,
|
||||
hash: hash,
|
||||
};
|
||||
}
|
||||
|
||||
public async getFileByHash(
|
||||
hash: string,
|
||||
databaseHashFetcher: (sha256: string) => Promise<string | null>
|
||||
databaseHashFetcher: (sha256: string) => Promise<string | null>,
|
||||
): Promise<File | null> {
|
||||
const filename = await databaseHashFetcher(hash);
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ export class LocalMediaBackend extends MediaBackend {
|
|||
|
||||
public async getFile(filename: string): Promise<File | null> {
|
||||
const file = Bun.file(
|
||||
`${this.config.media.local_uploads_folder}/${filename}`
|
||||
`${this.config.media.local_uploads_folder}/${filename}`,
|
||||
);
|
||||
|
||||
if (!(await file.exists())) return null;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
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 { MediaConverter } from "../media-converter";
|
||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||
import type { ConfigType } from "config-manager";
|
||||
|
||||
export class S3MediaBackend extends MediaBackend {
|
||||
constructor(
|
||||
config: ConfigType,
|
||||
config: Config,
|
||||
private s3Client = new S3Client({
|
||||
endPoint: config.s3.endpoint,
|
||||
useSSL: true,
|
||||
|
|
@ -14,37 +14,42 @@ export class S3MediaBackend extends MediaBackend {
|
|||
bucket: config.s3.bucket_name,
|
||||
accessKey: config.s3.access_key,
|
||||
secretKey: config.s3.secret_access_key,
|
||||
})
|
||||
}),
|
||||
) {
|
||||
super(config, MediaBackendType.S3);
|
||||
}
|
||||
|
||||
public async addFile(file: File) {
|
||||
let convertedFile = file;
|
||||
if (this.shouldConvertImages(this.config)) {
|
||||
const fileExtension = file.name.split(".").pop();
|
||||
const mediaConverter = new MediaConverter(
|
||||
fileExtension as ConvertableMediaFormats,
|
||||
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(), {
|
||||
size: file.size,
|
||||
});
|
||||
await this.s3Client.putObject(
|
||||
convertedFile.name,
|
||||
convertedFile.stream(),
|
||||
{
|
||||
size: convertedFile.size,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
uploadedFile: file,
|
||||
uploadedFile: convertedFile,
|
||||
hash: hash,
|
||||
};
|
||||
}
|
||||
|
||||
public async getFileByHash(
|
||||
hash: string,
|
||||
databaseHashFetcher: (sha256: string) => Promise<string | null>
|
||||
databaseHashFetcher: (sha256: string) => Promise<string | null>,
|
||||
): Promise<File | null> {
|
||||
const filename = await databaseHashFetcher(hash);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ConfigType } from "config-manager";
|
||||
import type { Config } from "config-manager";
|
||||
|
||||
export enum MediaBackendType {
|
||||
LOCAL = "local",
|
||||
|
|
@ -28,25 +28,25 @@ export class MediaHasher {
|
|||
|
||||
export class MediaBackend {
|
||||
constructor(
|
||||
public config: ConfigType,
|
||||
public backend: MediaBackendType
|
||||
public config: Config,
|
||||
public backend: MediaBackendType,
|
||||
) {}
|
||||
|
||||
static async fromBackendType(
|
||||
backend: MediaBackendType,
|
||||
config: ConfigType
|
||||
config: Config,
|
||||
): Promise<MediaBackend> {
|
||||
switch (backend) {
|
||||
case MediaBackendType.LOCAL:
|
||||
return new (await import("./backends/local")).LocalMediaBackend(
|
||||
config
|
||||
config,
|
||||
);
|
||||
case MediaBackendType.S3:
|
||||
return new (await import("./backends/s3")).S3MediaBackend(
|
||||
config
|
||||
config,
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
public shouldConvertImages(config: ConfigType) {
|
||||
public shouldConvertImages(config: Config) {
|
||||
return config.media.conversion.convert_images;
|
||||
}
|
||||
|
||||
|
|
@ -68,10 +68,10 @@ export class MediaBackend {
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
file: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
databaseHashFetcher: (sha256: string) => Promise<string>
|
||||
databaseHashFetcher: (sha256: string) => Promise<string>,
|
||||
): Promise<File | null> {
|
||||
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
|
||||
public getFile(filename: string): Promise<File | null> {
|
||||
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
|
||||
public addFile(file: File): Promise<UploadedFileMetadata> {
|
||||
return Promise.reject(
|
||||
new Error("Do not call MediaBackend directly: use a subclass")
|
||||
new Error("Do not call MediaBackend directly: use a subclass"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export enum ConvertableMediaFormats {
|
|||
export class MediaConverter {
|
||||
constructor(
|
||||
public fromFormat: ConvertableMediaFormats,
|
||||
public toFormat: ConvertableMediaFormats
|
||||
public toFormat: ConvertableMediaFormats,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -43,7 +43,7 @@ export class MediaConverter {
|
|||
private getReplacedFileName(fileName: string) {
|
||||
return this.extractFilenameFromPath(fileName).replace(
|
||||
new RegExp(`\\.${this.fromFormat}$`),
|
||||
`.${this.toFormat}`
|
||||
`.${this.toFormat}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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 { S3MediaBackend } from "../backends/s3";
|
||||
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
|
|
@ -13,7 +13,7 @@ type DeepPartial<T> = {
|
|||
|
||||
describe("MediaBackend", () => {
|
||||
let mediaBackend: MediaBackend;
|
||||
let mockConfig: ConfigType;
|
||||
let mockConfig: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
|
|
@ -22,7 +22,7 @@ describe("MediaBackend", () => {
|
|||
convert_images: true,
|
||||
},
|
||||
},
|
||||
} as ConfigType;
|
||||
} as Config;
|
||||
mediaBackend = new MediaBackend(mockConfig, MediaBackendType.S3);
|
||||
});
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ describe("MediaBackend", () => {
|
|||
it("should return a LocalMediaBackend instance for LOCAL backend type", async () => {
|
||||
const backend = await MediaBackend.fromBackendType(
|
||||
MediaBackendType.LOCAL,
|
||||
mockConfig
|
||||
mockConfig,
|
||||
);
|
||||
expect(backend).toBeInstanceOf(LocalMediaBackend);
|
||||
});
|
||||
|
|
@ -51,14 +51,15 @@ describe("MediaBackend", () => {
|
|||
public_url: "test",
|
||||
secret_access_key: "test-secret",
|
||||
},
|
||||
} as ConfigType
|
||||
} as Config,
|
||||
);
|
||||
expect(backend).toBeInstanceOf(S3MediaBackend);
|
||||
});
|
||||
|
||||
it("should throw an error for unknown backend type", () => {
|
||||
expect(
|
||||
MediaBackend.fromBackendType("unknown" as any, mockConfig)
|
||||
// @ts-expect-error This is a test
|
||||
MediaBackend.fromBackendType("unknown", mockConfig),
|
||||
).rejects.toThrow("Unknown backend type: unknown");
|
||||
});
|
||||
});
|
||||
|
|
@ -74,7 +75,7 @@ describe("MediaBackend", () => {
|
|||
const databaseHashFetcher = jest.fn().mockResolvedValue("test.jpg");
|
||||
|
||||
expect(
|
||||
mediaBackend.getFileByHash(mockHash, databaseHashFetcher)
|
||||
mediaBackend.getFileByHash(mockHash, databaseHashFetcher),
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
|
|
@ -94,7 +95,7 @@ describe("MediaBackend", () => {
|
|||
describe("S3MediaBackend", () => {
|
||||
let s3MediaBackend: S3MediaBackend;
|
||||
let mockS3Client: Partial<S3Client>;
|
||||
let mockConfig: DeepPartial<ConfigType>;
|
||||
let mockConfig: DeepPartial<Config>;
|
||||
let mockFile: File;
|
||||
let mockMediaHasher: MediaHasher;
|
||||
|
||||
|
|
@ -125,8 +126,8 @@ describe("S3MediaBackend", () => {
|
|||
}),
|
||||
} as Partial<S3Client>;
|
||||
s3MediaBackend = new S3MediaBackend(
|
||||
mockConfig as ConfigType,
|
||||
mockS3Client as S3Client
|
||||
mockConfig as Config,
|
||||
mockS3Client as S3Client,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -145,7 +146,7 @@ describe("S3MediaBackend", () => {
|
|||
expect(mockS3Client.putObject).toHaveBeenCalledWith(
|
||||
mockFile.name,
|
||||
expect.any(ReadableStream),
|
||||
{ size: mockFile.size }
|
||||
{ size: mockFile.size },
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -161,7 +162,7 @@ describe("S3MediaBackend", () => {
|
|||
|
||||
const file = await s3MediaBackend.getFileByHash(
|
||||
mockHash,
|
||||
databaseHashFetcher
|
||||
databaseHashFetcher,
|
||||
);
|
||||
|
||||
expect(file).not.toBeNull();
|
||||
|
|
@ -187,7 +188,7 @@ describe("S3MediaBackend", () => {
|
|||
|
||||
describe("LocalMediaBackend", () => {
|
||||
let localMediaBackend: LocalMediaBackend;
|
||||
let mockConfig: ConfigType;
|
||||
let mockConfig: Config;
|
||||
let mockFile: File;
|
||||
let mockMediaHasher: MediaHasher;
|
||||
|
||||
|
|
@ -200,15 +201,15 @@ describe("LocalMediaBackend", () => {
|
|||
},
|
||||
local_uploads_folder: "./uploads",
|
||||
},
|
||||
} as ConfigType;
|
||||
mockFile = Bun.file(__dirname + "/megamind.jpg") as unknown as File;
|
||||
} as Config;
|
||||
mockFile = Bun.file(`${__dirname}/megamind.jpg`) as unknown as File;
|
||||
mockMediaHasher = new MediaHasher();
|
||||
localMediaBackend = new LocalMediaBackend(mockConfig);
|
||||
});
|
||||
|
||||
it("should initialize with correct type", () => {
|
||||
expect(localMediaBackend.getBackendType()).toEqual(
|
||||
MediaBackendType.LOCAL
|
||||
MediaBackendType.LOCAL,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -217,7 +218,7 @@ describe("LocalMediaBackend", () => {
|
|||
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
|
||||
const mockMediaConverter = new MediaConverter(
|
||||
ConvertableMediaFormats.JPG,
|
||||
ConvertableMediaFormats.PNG
|
||||
ConvertableMediaFormats.PNG,
|
||||
);
|
||||
spyOn(mockMediaConverter, "convert").mockResolvedValue(mockFile);
|
||||
// @ts-expect-error This is a mock
|
||||
|
|
@ -225,13 +226,13 @@ describe("LocalMediaBackend", () => {
|
|||
exists: () => Promise.resolve(false),
|
||||
}));
|
||||
spyOn(Bun, "write").mockImplementationOnce(() =>
|
||||
Promise.resolve(mockFile.size)
|
||||
Promise.resolve(mockFile.size),
|
||||
);
|
||||
|
||||
const result = await localMediaBackend.addFile(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);
|
||||
});
|
||||
|
||||
|
|
@ -249,7 +250,7 @@ describe("LocalMediaBackend", () => {
|
|||
|
||||
const file = await localMediaBackend.getFileByHash(
|
||||
mockHash,
|
||||
databaseHashFetcher
|
||||
databaseHashFetcher,
|
||||
);
|
||||
|
||||
expect(file).not.toBeNull();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/media-converter.test.ts
|
||||
import { describe, it, expect, beforeEach } from "bun:test";
|
||||
import { MediaConverter, ConvertableMediaFormats } from "../media-converter";
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
|
||||
|
||||
describe("MediaConverter", () => {
|
||||
let mediaConverter: MediaConverter;
|
||||
|
|
@ -8,7 +8,7 @@ describe("MediaConverter", () => {
|
|||
beforeEach(() => {
|
||||
mediaConverter = new MediaConverter(
|
||||
ConvertableMediaFormats.JPG,
|
||||
ConvertableMediaFormats.PNG
|
||||
ConvertableMediaFormats.PNG,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -27,8 +27,8 @@ describe("MediaConverter", () => {
|
|||
const fileName = "test.jpg";
|
||||
const expectedFileName = "test.png";
|
||||
// Written like this because it's a private function
|
||||
expect(mediaConverter["getReplacedFileName"](fileName)).toEqual(
|
||||
expectedFileName
|
||||
expect(mediaConverter.getReplacedFileName(fileName)).toEqual(
|
||||
expectedFileName,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -36,30 +36,30 @@ describe("MediaConverter", () => {
|
|||
it("should extract filename from path", () => {
|
||||
const path = "path/to/test.jpg";
|
||||
const expectedFileName = "test.jpg";
|
||||
expect(mediaConverter["extractFilenameFromPath"](path)).toEqual(
|
||||
expectedFileName
|
||||
expect(mediaConverter.extractFilenameFromPath(path)).toEqual(
|
||||
expectedFileName,
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle escaped slashes", () => {
|
||||
const path = "path/to/test\\/test.jpg";
|
||||
const expectedFileName = "test\\/test.jpg";
|
||||
expect(mediaConverter["extractFilenameFromPath"](path)).toEqual(
|
||||
expectedFileName
|
||||
expect(mediaConverter.extractFilenameFromPath(path)).toEqual(
|
||||
expectedFileName,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should convert media", async () => {
|
||||
const file = Bun.file(__dirname + "/megamind.jpg");
|
||||
const file = Bun.file(`${__dirname}/megamind.jpg`);
|
||||
|
||||
const convertedFile = await mediaConverter.convert(
|
||||
file as unknown as File
|
||||
file as unknown as File,
|
||||
);
|
||||
|
||||
expect(convertedFile.name).toEqual("megamind.png");
|
||||
expect(convertedFile.type).toEqual(
|
||||
`image/${ConvertableMediaFormats.PNG}`
|
||||
`image/${ConvertableMediaFormats.PNG}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ export enum SupportedProtocols {
|
|||
* This class is not meant to be instantiated directly, but rather for its children to be used.
|
||||
*/
|
||||
export class ProtocolTranslator {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
static auto(object: any) {
|
||||
const protocol = this.recognizeProtocol(object);
|
||||
const protocol = ProtocolTranslator.recognizeProtocol(object);
|
||||
switch (protocol) {
|
||||
case SupportedProtocols.ACTIVITYPUB:
|
||||
return new ActivityPubTranslator();
|
||||
|
|
@ -41,6 +42,8 @@ export class ProtocolTranslator {
|
|||
/**
|
||||
* Automatically recognizes the protocol of a given object
|
||||
*/
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
private static recognizeProtocol(object: any) {
|
||||
// Temporary stub
|
||||
return SupportedProtocols.ACTIVITYPUB;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import { ProtocolTranslator } from "..";
|
||||
|
||||
export class ActivityPubTranslator extends ProtocolTranslator {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
user() {
|
||||
|
||||
}
|
||||
user() {}
|
||||
}
|
||||
|
|
@ -88,16 +88,16 @@ export class RequestParser {
|
|||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
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("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
|
||||
(result[arrayKey] as any[]).push(value);
|
||||
(result[arrayKey] as FormDataEntryValue[]).push(value);
|
||||
} 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 any[]).push(value);
|
||||
(result[arrayKey] as FormDataEntryValue[]).push(value);
|
||||
} 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);
|
||||
} else {
|
||||
result[key as keyof T] = value as any;
|
||||
result[key as keyof T] = value as T[keyof T];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { describe, it, expect, test } from "bun:test";
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { RequestParser } from "..";
|
||||
|
||||
describe("RequestParser", () => {
|
||||
describe("Should parse query parameters correctly", () => {
|
||||
test("With text parameters", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?param1=value1¶m2=value2"
|
||||
"http://localhost?param1=value1¶m2=value2",
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
|
|
@ -16,7 +16,7 @@ describe("RequestParser", () => {
|
|||
|
||||
test("With Array", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?test[]=value1&test[]=value2"
|
||||
"http://localhost?test[]=value1&test[]=value2",
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
test: string[];
|
||||
|
|
@ -26,7 +26,7 @@ describe("RequestParser", () => {
|
|||
|
||||
test("With both at once", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?param1=value1¶m2=value2&test[]=value1&test[]=value2"
|
||||
"http://localhost?param1=value1¶m2=value2&test[]=value1&test[]=value2",
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
// Import Tailwind style reset
|
||||
import '@unocss/reset/tailwind-compat.css'
|
||||
import "@unocss/reset/tailwind-compat.css";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
label: string;
|
||||
|
|
@ -26,5 +26,5 @@ const checkValid = (e: Event) => {
|
|||
} else {
|
||||
isInvalid.value = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import "virtual:uno.css";
|
||||
import { createApp } from "vue";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import App from "./App.vue";
|
||||
import routes from "./routes";
|
||||
import "./style.css";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref } from "vue";
|
||||
|
||||
const location = window.location;
|
||||
const version = __VERSION__;
|
||||
|
|
|
|||
|
|
@ -52,9 +52,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import LoginInput from "../../components/LoginInput.vue"
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import LoginInput from "../../components/LoginInput.vue";
|
||||
|
||||
const query = useRoute().query;
|
||||
|
||||
|
|
@ -64,18 +64,21 @@ const client_id = query.client_id;
|
|||
const scope = query.scope;
|
||||
const error = decodeURIComponent(query.error as string);
|
||||
|
||||
const oauthProviders = ref<{
|
||||
const oauthProviders = ref<
|
||||
| {
|
||||
name: string;
|
||||
icon: string;
|
||||
id: string
|
||||
}[] | null>(null);
|
||||
id: string;
|
||||
}[]
|
||||
| null
|
||||
>(null);
|
||||
|
||||
const getOauthProviders = async () => {
|
||||
const response = await fetch('/oauth/providers');
|
||||
return await response.json() as any;
|
||||
}
|
||||
const response = await fetch("/oauth/providers");
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
oauthProviders.value = await getOauthProviders();
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const query = useRoute().query;
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ const application = query.application;
|
|||
const website = decodeURIComponent(query.website as string);
|
||||
const redirect_uri = query.redirect_uri as string;
|
||||
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 oauthScopeText: Record<string, string> = {
|
||||
|
|
@ -79,7 +79,7 @@ const oauthScopeText: Record<string, string> = {
|
|||
"w:conversations": "Edit your conversations",
|
||||
"w:media": "Upload media",
|
||||
"w:reports": "Report users",
|
||||
}
|
||||
};
|
||||
|
||||
const scopes = scope.split(" ");
|
||||
|
||||
|
|
@ -89,30 +89,56 @@ const scopes = scope.split(" ");
|
|||
// 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
|
||||
const getScopeText = (fullScopes: string[]) => {
|
||||
let scopeTexts = [];
|
||||
const scopeTexts = [];
|
||||
|
||||
const readScopes = fullScopes.filter(scope => scope.includes("read"));
|
||||
const writeScopes = fullScopes.filter(scope => scope.includes("write"));
|
||||
const readScopes = fullScopes.filter((scope) => scope.includes("read"));
|
||||
const writeScopes = fullScopes.filter((scope) => scope.includes("write"));
|
||||
|
||||
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 (oauthScopeText[possibleScope].includes("$VERB")) scopeTexts.push(["Read and write", oauthScopeText[possibleScope].replace("$VERB", "")]);
|
||||
if (
|
||||
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]]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scopeAction.includes('r') && (readScopes.includes(`read:${scopeName}`) || readScopes.find(scope => scope === "read"))) {
|
||||
if (oauthScopeText[possibleScope].includes("$VERB")) scopeTexts.push(["Read", oauthScopeText[possibleScope].replace("$VERB", "")]);
|
||||
if (
|
||||
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]]);
|
||||
}
|
||||
|
||||
if (scopeAction.includes('w') && (writeScopes.includes(`write:${scopeName}`) || writeScopes.find(scope => scope === "write"))) {
|
||||
if (oauthScopeText[possibleScope].includes("$VERB")) scopeTexts.push(["Write", oauthScopeText[possibleScope].replace("$VERB", "")]);
|
||||
if (
|
||||
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]]);
|
||||
}
|
||||
}
|
||||
return scopeTexts;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -98,12 +98,14 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import type { APIInstance } from "~types/entities/instance";
|
||||
import LoginInput from "../../components/LoginInput.vue"
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import LoginInput from "../../components/LoginInput.vue";
|
||||
|
||||
const instanceInfo = await fetch("/api/v1/instance").then(res => res.json()) as APIInstance & {
|
||||
tos_url: string
|
||||
const instanceInfo = (await fetch("/api/v1/instance").then((res) =>
|
||||
res.json(),
|
||||
)) as APIInstance & {
|
||||
tos_url: string;
|
||||
};
|
||||
|
||||
const errors = ref<{
|
||||
|
|
@ -124,26 +126,40 @@ const registerUser = (e: Event) => {
|
|||
e.preventDefault();
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("email", (e.target as any).email.value);
|
||||
formData.append("password", (e.target as any).password.value);
|
||||
formData.append("username", (e.target as any).username.value);
|
||||
const target = e.target as unknown as Record<string, HTMLInputElement>;
|
||||
|
||||
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("locale", "en")
|
||||
formData.append("locale", "en");
|
||||
formData.append("agreement", "true");
|
||||
// @ts-ignore
|
||||
fetch("/api/v1/accounts", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}).then(async res => {
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.status === 422) {
|
||||
errors.value = (await res.json() as any).details;
|
||||
console.log(errors.value)
|
||||
errors.value = (
|
||||
(await res.json()) as Record<
|
||||
string,
|
||||
{
|
||||
[key: string]: {
|
||||
error: string;
|
||||
description: string;
|
||||
}[];
|
||||
}
|
||||
>
|
||||
).details;
|
||||
console.log(errors.value);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
window.location.href = "/register/success";
|
||||
}
|
||||
}).catch(async err => {
|
||||
console.error(err);
|
||||
})
|
||||
}
|
||||
.catch(async (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineConfig } from "vite";
|
||||
import UnoCSS from "unocss/vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import UnoCSS from "unocss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import pkg from "../package.json";
|
||||
|
||||
export default defineConfig({
|
||||
|
|
|
|||
|
|
@ -97,58 +97,58 @@ interface ServerEvents {
|
|||
req: Request,
|
||||
obj: LysandObjectType,
|
||||
author: UserWithRelations,
|
||||
publicKey: string
|
||||
publicKey: string,
|
||||
) => void;
|
||||
[HookTypes.OnCryptoSuccess]: (
|
||||
req: Request,
|
||||
obj: LysandObjectType,
|
||||
author: UserWithRelations,
|
||||
publicKey: string
|
||||
publicKey: string,
|
||||
) => void;
|
||||
[HookTypes.OnBan]: (
|
||||
req: Request,
|
||||
bannedUser: UserWithRelations,
|
||||
banner: UserWithRelations
|
||||
banner: UserWithRelations,
|
||||
) => void;
|
||||
[HookTypes.OnSuspend]: (
|
||||
req: Request,
|
||||
suspendedUser: UserWithRelations,
|
||||
suspender: UserWithRelations
|
||||
suspender: UserWithRelations,
|
||||
) => void;
|
||||
[HookTypes.OnUserBlock]: (
|
||||
req: Request,
|
||||
blockedUser: UserWithRelations,
|
||||
blocker: UserWithRelations
|
||||
blocker: UserWithRelations,
|
||||
) => void;
|
||||
[HookTypes.OnUserMute]: (
|
||||
req: Request,
|
||||
mutedUser: UserWithRelations,
|
||||
muter: UserWithRelations
|
||||
muter: UserWithRelations,
|
||||
) => void;
|
||||
[HookTypes.OnUserFollow]: (
|
||||
req: Request,
|
||||
followedUser: UserWithRelations,
|
||||
follower: UserWithRelations
|
||||
follower: UserWithRelations,
|
||||
) => void;
|
||||
[HookTypes.OnRegister]: (req: Request, newUser: UserWithRelations) => void;
|
||||
[HookTypes.OnDeleteAccount]: (
|
||||
req: Request,
|
||||
deletedUser: UserWithRelations
|
||||
deletedUser: UserWithRelations,
|
||||
) => void;
|
||||
[HookTypes.OnPostCreate]: (
|
||||
req: Request,
|
||||
newPost: StatusWithRelations,
|
||||
author: UserWithRelations
|
||||
author: UserWithRelations,
|
||||
) => void;
|
||||
[HookTypes.OnPostDelete]: (
|
||||
req: Request,
|
||||
deletedPost: StatusWithRelations,
|
||||
deleter: UserWithRelations
|
||||
deleter: UserWithRelations,
|
||||
) => void;
|
||||
[HookTypes.OnPostUpdate]: (
|
||||
req: Request,
|
||||
updatedPost: StatusWithRelations,
|
||||
updater: UserWithRelations
|
||||
updater: UserWithRelations,
|
||||
) => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { config } from "config-manager";
|
|||
// Proxies all `bunx prisma` commands with an environment variable
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export const rawRoutes = {
|
|||
// Returns the route filesystem path when given a URL
|
||||
export const routeMatcher = new Bun.FileSystemRouter({
|
||||
style: "nextjs",
|
||||
dir: process.cwd() + "/server/api",
|
||||
dir: `${process.cwd()}/server/api`,
|
||||
});
|
||||
|
||||
export const matchRoute = async <T = Record<string, never>>(url: string) => {
|
||||
|
|
|
|||
75
server.ts
75
server.ts
|
|
@ -1,16 +1,16 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import type { Config } from "config-manager";
|
||||
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 { LogLevel } from "log-manager";
|
||||
import { RequestParser } from "request-parser";
|
||||
import { getFromRequest } from "~database/entities/User";
|
||||
import { matchRoute } from "~routes";
|
||||
|
||||
export const createServer = (
|
||||
config: Config,
|
||||
logger: LogManager | MultiLogManager,
|
||||
isProd: boolean
|
||||
isProd: boolean,
|
||||
) =>
|
||||
Bun.serve({
|
||||
port: config.http.bind_port,
|
||||
|
|
@ -52,22 +52,21 @@ export const createServer = (
|
|||
if (matches(ip, request_ip)) {
|
||||
const file = Bun.file(
|
||||
config.http.bait.send_file ||
|
||||
"./pages/beemovie.txt"
|
||||
"./pages/beemovie.txt",
|
||||
);
|
||||
|
||||
if (await file.exists()) {
|
||||
return new Response(file);
|
||||
} else {
|
||||
}
|
||||
await logger.log(
|
||||
LogLevel.ERROR,
|
||||
"Server.Bait",
|
||||
`Bait file not found: ${config.http.bait.send_file}`
|
||||
`Bait file not found: ${config.http.bait.send_file}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[-] Error while parsing bait IP "${ip}" `
|
||||
`[-] Error while parsing bait IP "${ip}" `,
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
|
@ -78,27 +77,27 @@ export const createServer = (
|
|||
console.log(agent);
|
||||
if (new RegExp(agent).test(ua)) {
|
||||
const file = Bun.file(
|
||||
config.http.bait.send_file || "./pages/beemovie.txt"
|
||||
config.http.bait.send_file ||
|
||||
"./pages/beemovie.txt",
|
||||
);
|
||||
|
||||
if (await file.exists()) {
|
||||
return new Response(file);
|
||||
} else {
|
||||
}
|
||||
await logger.log(
|
||||
LogLevel.ERROR,
|
||||
"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) {
|
||||
await logger.logRequest(
|
||||
req,
|
||||
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(
|
||||
req.url
|
||||
req.url,
|
||||
);
|
||||
|
||||
const file = filePromise;
|
||||
|
||||
if (matchedRoute && file == undefined) {
|
||||
if (matchedRoute && file === undefined) {
|
||||
await logger.log(
|
||||
LogLevel.ERROR,
|
||||
"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);
|
||||
}
|
||||
|
||||
if (
|
||||
matchedRoute &&
|
||||
matchedRoute.name !== "/[...404]" &&
|
||||
file != undefined
|
||||
) {
|
||||
if (matchedRoute && matchedRoute.name !== "/[...404]" && file) {
|
||||
const meta = file.meta;
|
||||
|
||||
// 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, {
|
||||
status: 405,
|
||||
statusText: `Method not allowed: allowed methods are: ${meta.allowedMethods.join(
|
||||
", "
|
||||
", ",
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
|
|
@ -151,9 +147,8 @@ export const createServer = (
|
|||
});
|
||||
}
|
||||
} else if (
|
||||
(meta.auth.requiredOnMethods ?? []).includes(
|
||||
req.method as any
|
||||
)
|
||||
// @ts-expect-error Stupid error
|
||||
(meta.auth.requiredOnMethods ?? []).includes(req.method)
|
||||
) {
|
||||
if (!auth.user) {
|
||||
return new Response(undefined, {
|
||||
|
|
@ -171,7 +166,7 @@ export const createServer = (
|
|||
await logger.logError(
|
||||
LogLevel.ERROR,
|
||||
"Server.RouteRequestParser",
|
||||
e as Error
|
||||
e as Error,
|
||||
);
|
||||
return new Response(undefined, {
|
||||
status: 400,
|
||||
|
|
@ -187,7 +182,8 @@ export const createServer = (
|
|||
getConfig: () => Promise.resolve(config),
|
||||
},
|
||||
});
|
||||
} else if (matchedRoute?.name === "/[...404]" || !matchedRoute) {
|
||||
}
|
||||
if (matchedRoute?.name === "/[...404]" || !matchedRoute) {
|
||||
if (new URL(req.url).pathname.startsWith("/api")) {
|
||||
return errorResponse("Route not found", 404);
|
||||
}
|
||||
|
|
@ -196,41 +192,42 @@ export const createServer = (
|
|||
if (isProd) {
|
||||
if (new URL(req.url).pathname.startsWith("/assets")) {
|
||||
const file = Bun.file(
|
||||
`./pages/dist${new URL(req.url).pathname}`
|
||||
`./pages/dist${new URL(req.url).pathname}`,
|
||||
);
|
||||
|
||||
// Serve from pages/dist/assets
|
||||
if (await file.exists()) {
|
||||
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")) {
|
||||
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
|
||||
return new Response(file);
|
||||
} else {
|
||||
}
|
||||
const proxy = await fetch(
|
||||
req.url.replace(
|
||||
config.http.base_url,
|
||||
"http://localhost:5173"
|
||||
)
|
||||
).catch(async e => {
|
||||
"http://localhost:5173",
|
||||
),
|
||||
).catch(async (e) => {
|
||||
await logger.logError(
|
||||
LogLevel.ERROR,
|
||||
"Server.Proxy",
|
||||
e as Error
|
||||
e as Error,
|
||||
);
|
||||
await logger.log(
|
||||
LogLevel.ERROR,
|
||||
"Server.Proxy",
|
||||
`The development Vite server is not running or the route is not found: ${req.url.replace(
|
||||
config.http.base_url,
|
||||
"http://localhost:5173"
|
||||
)}`
|
||||
"http://localhost:5173",
|
||||
)}`,
|
||||
);
|
||||
return errorResponse("Route not found", 404);
|
||||
});
|
||||
|
|
@ -244,8 +241,6 @@ export const createServer = (
|
|||
|
||||
return errorResponse("Route not found", 404);
|
||||
}
|
||||
} else {
|
||||
return errorResponse("Route not found", 404);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { xmlResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { xmlResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -13,7 +13,6 @@ export const meta = applyConfig({
|
|||
route: "/.well-known/host-meta",
|
||||
});
|
||||
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { jsonResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -22,22 +22,28 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
name: config.instance.name,
|
||||
version: "0.0.1",
|
||||
description: config.instance.description,
|
||||
logo: config.instance.logo ? [
|
||||
logo: config.instance.logo
|
||||
? [
|
||||
{
|
||||
content: config.instance.logo,
|
||||
content_type: `image/${config.instance.logo.split(".")[1]}`,
|
||||
}
|
||||
] : undefined,
|
||||
banner: config.instance.banner ? [
|
||||
content_type: `image/${
|
||||
config.instance.logo.split(".")[1]
|
||||
}`,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
banner: config.instance.banner
|
||||
? [
|
||||
{
|
||||
content: config.instance.banner,
|
||||
content_type: `image/${config.instance.banner.split(".")[1]}`,
|
||||
}
|
||||
] : undefined,
|
||||
supported_extensions: [
|
||||
"org.lysand:custom_emojis"
|
||||
],
|
||||
content_type: `image/${
|
||||
config.instance.banner.split(".")[1]
|
||||
}`,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
supported_extensions: ["org.lysand:custom_emojis"],
|
||||
website: "https://lysand.org",
|
||||
// TODO: Add admins, moderators field
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ export const meta = applyConfig({
|
|||
route: "/.well-known/nodeinfo",
|
||||
});
|
||||
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -42,18 +42,18 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
{
|
||||
rel: "self",
|
||||
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",
|
||||
type: "text/html",
|
||||
href: `${config.http.base_url}/users/${user.username}`
|
||||
href: `${config.http.base_url}/users/${user.username}`,
|
||||
},
|
||||
{
|
||||
rel: "self",
|
||||
type: "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||
href: `${config.http.base_url}/users/${user.username}/actor`
|
||||
}
|
||||
]
|
||||
})
|
||||
type: 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
href: `${config.http.base_url}/users/${user.username}/actor`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -68,15 +68,15 @@ export default apiRoute<{
|
|||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${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(
|
||||
await Promise.all(objects.map(object => userToAPI(object))),
|
||||
await Promise.all(objects.map((object) => userToAPI(object))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -68,15 +68,15 @@ export default apiRoute<{
|
|||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${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(
|
||||
await Promise.all(objects.map(object => userToAPI(object))),
|
||||
await Promise.all(objects.map((object) => userToAPI(object))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import type { UserWithRelations } 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";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { statusToAPI } from "~database/entities/Status";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
import { statusToAPI } from "~database/entities/Status";
|
||||
import {
|
||||
userRelations,
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -84,16 +84,18 @@ export default apiRoute<{
|
|||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${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(
|
||||
await Promise.all(objects.map(status => statusToAPI(status, user))),
|
||||
await Promise.all(
|
||||
objects.map((status) => statusToAPI(status, user)),
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -120,15 +122,15 @@ export default apiRoute<{
|
|||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${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(
|
||||
await Promise.all(objects.map(status => statusToAPI(status, user))),
|
||||
await Promise.all(objects.map((status) => statusToAPI(status, user))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -54,7 +54,7 @@ export default apiRoute<{
|
|||
some: {
|
||||
ownerId: self.id,
|
||||
subjectId: {
|
||||
in: followersOfIds.map(f => f.id),
|
||||
in: followersOfIds.map((f) => f.id),
|
||||
},
|
||||
following: true,
|
||||
},
|
||||
|
|
@ -63,5 +63,5 @@ export default apiRoute<{
|
|||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(output.map(o => userToAPI(o)));
|
||||
return jsonResponse(output.map((o) => userToAPI(o)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { jsonResponse } from "@response";
|
||||
import { tempmailDomains } from "@tempmail";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { client } from "~database/datasource";
|
||||
import { createNewLocalUser } from "~database/entities/User";
|
||||
import ISO6391 from "iso-639-1";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
@ -37,7 +37,7 @@ export default apiRoute<{
|
|||
{
|
||||
error: "Registration is disabled",
|
||||
},
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -70,33 +70,37 @@ export default apiRoute<{
|
|||
};
|
||||
|
||||
// Check if fields are blank
|
||||
["username", "email", "password", "agreement", "locale", "reason"].forEach(
|
||||
value => {
|
||||
// @ts-expect-error Value is always valid
|
||||
if (!body[value])
|
||||
for (const value of [
|
||||
"username",
|
||||
"email",
|
||||
"password",
|
||||
"agreement",
|
||||
"locale",
|
||||
"reason",
|
||||
]) {
|
||||
// @ts-expect-error We don't care about typing here
|
||||
if (!body[value]) {
|
||||
errors.details[value].push({
|
||||
error: "ERR_BLANK",
|
||||
description: `can't be blank`,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Check if username is valid
|
||||
if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
|
||||
errors.details.username.push({
|
||||
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
|
||||
if (
|
||||
config.filters.username_filters.some(filter =>
|
||||
body.username?.match(filter)
|
||||
)
|
||||
config.filters.username.some((filter) => body.username?.match(filter))
|
||||
) {
|
||||
errors.details.username.push({
|
||||
error: "ERR_INVALID",
|
||||
description: `contains blocked words`,
|
||||
description: "contains blocked words",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -111,32 +115,32 @@ export default apiRoute<{
|
|||
if ((body.username?.length ?? 0) < 3)
|
||||
errors.details.username.push({
|
||||
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
|
||||
if (config.validation.username_blacklist.includes(body.username ?? ""))
|
||||
errors.details.username.push({
|
||||
error: "ERR_RESERVED",
|
||||
description: `is reserved`,
|
||||
description: "is reserved",
|
||||
});
|
||||
|
||||
// Check if username is taken
|
||||
if (await client.user.findFirst({ where: { username: body.username } }))
|
||||
errors.details.username.push({
|
||||
error: "ERR_TAKEN",
|
||||
description: `is already taken`,
|
||||
description: "is already taken",
|
||||
});
|
||||
|
||||
// Check if email is valid
|
||||
if (
|
||||
!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({
|
||||
error: "ERR_INVALID",
|
||||
description: `must be a valid email address`,
|
||||
description: "must be a valid email address",
|
||||
});
|
||||
|
||||
// Check if email is blocked
|
||||
|
|
@ -147,14 +151,14 @@ export default apiRoute<{
|
|||
)
|
||||
errors.details.email.push({
|
||||
error: "ERR_BLOCKED",
|
||||
description: `is from a blocked email provider`,
|
||||
description: "is from a blocked email provider",
|
||||
});
|
||||
|
||||
// Check if agreement is accepted
|
||||
if (!body.agreement)
|
||||
errors.details.agreement.push({
|
||||
error: "ERR_ACCEPTED",
|
||||
description: `must be accepted`,
|
||||
description: "must be accepted",
|
||||
});
|
||||
|
||||
if (!body.locale)
|
||||
|
|
@ -166,19 +170,19 @@ export default apiRoute<{
|
|||
if (!ISO6391.validate(body.locale ?? ""))
|
||||
errors.details.locale.push({
|
||||
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 (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"
|
||||
|
||||
const errorsText = Object.entries(errors.details)
|
||||
.map(
|
||||
([name, errors]) =>
|
||||
`${name} ${errors
|
||||
.map(error => error.description)
|
||||
.join(", ")}`
|
||||
.map((error) => error.description)
|
||||
.join(", ")}`,
|
||||
)
|
||||
.join(", ");
|
||||
return jsonResponse(
|
||||
|
|
@ -186,7 +190,7 @@ export default apiRoute<{
|
|||
error: `Validation failed: ${errorsText}`,
|
||||
details: errors.details,
|
||||
},
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import type { User } from "@prisma/client";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -47,20 +48,20 @@ export default apiRoute<{
|
|||
|
||||
// Find IDs that dont have a relationship
|
||||
const missingIds = ids.filter(
|
||||
id => !relationships.some(r => r.subjectId === id)
|
||||
(id) => !relationships.some((r) => r.subjectId === id),
|
||||
);
|
||||
|
||||
// Create the missing relationships
|
||||
for (const id of missingIds) {
|
||||
const relationship = await createNewRelationship(self, { id } as any);
|
||||
const relationship = await createNewRelationship(self, { id } as User);
|
||||
|
||||
relationships.push(relationship);
|
||||
}
|
||||
|
||||
// Order in the same order as ids
|
||||
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)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -71,5 +71,5 @@ export default apiRoute<{
|
|||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(accounts.map(acct => userToAPI(acct)));
|
||||
return jsonResponse(accounts.map((acct) => userToAPI(acct)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import { convertTextToHtml } from "@formatting";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { sanitizeHtml } from "@sanitization";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { parseEmojis } from "~database/entities/Emoji";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APISource } from "~types/entities/source";
|
||||
import { convertTextToHtml } from "@formatting";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import { MediaBackendType } 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 { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||
import { getUrl } from "~database/entities/Attachment";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
import type { APISource } from "~types/entities/source";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["PATCH"],
|
||||
|
|
@ -97,14 +97,14 @@ export default apiRoute<{
|
|||
) {
|
||||
return errorResponse(
|
||||
`Display name must be between 3 and ${config.validation.max_displayname_size} characters`,
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if display name doesnt match filters
|
||||
if (
|
||||
config.filters.displayname.some(filter =>
|
||||
sanitizedDisplayName.match(filter)
|
||||
config.filters.displayname.some((filter) =>
|
||||
sanitizedDisplayName.match(filter),
|
||||
)
|
||||
) {
|
||||
return errorResponse("Display name contains blocked words", 422);
|
||||
|
|
@ -121,12 +121,12 @@ export default apiRoute<{
|
|||
if (sanitizedNote.length > config.validation.max_note_size) {
|
||||
return errorResponse(
|
||||
`Note must be less than ${config.validation.max_note_size} characters`,
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
|
@ -139,12 +139,12 @@ export default apiRoute<{
|
|||
// Check if within allowed privacy values
|
||||
if (
|
||||
!["public", "unlisted", "private", "direct"].includes(
|
||||
source_privacy
|
||||
source_privacy,
|
||||
)
|
||||
) {
|
||||
return errorResponse(
|
||||
"Privacy must be one of public, unlisted, private, or direct",
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ export default apiRoute<{
|
|||
if (!ISO6391.validate(source_language)) {
|
||||
return errorResponse(
|
||||
"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) {
|
||||
return errorResponse(
|
||||
`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) {
|
||||
return errorResponse(
|
||||
`Header must be less than ${config.validation.max_avatar_size} bytes`,
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +235,8 @@ export default apiRoute<{
|
|||
|
||||
// Deduplicate emojis
|
||||
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({
|
||||
|
|
@ -249,10 +250,10 @@ export default apiRoute<{
|
|||
isBot: user.isBot,
|
||||
isDiscoverable: user.isDiscoverable,
|
||||
emojis: {
|
||||
disconnect: user.emojis.map(e => ({
|
||||
disconnect: user.emojis.map((e) => ({
|
||||
id: e.id,
|
||||
})),
|
||||
connect: user.emojis.map(e => ({
|
||||
connect: user.emojis.map((e) => ({
|
||||
id: e.id,
|
||||
})),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { randomBytes } from "node:crypto";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { randomBytes } from "crypto";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -35,7 +35,7 @@ export default apiRoute<{
|
|||
if (!redirect_uri.protocol.startsWith("http")) {
|
||||
return errorResponse(
|
||||
"Redirect URI must be an absolute URI",
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -33,5 +33,5 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(blocks.map(u => userToAPI(u)));
|
||||
return jsonResponse(blocks.map((u) => userToAPI(u)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ export default apiRoute(async () => {
|
|||
});
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(emojis.map(emoji => emojiToAPI(emoji)))
|
||||
await Promise.all(emojis.map((emoji) => emojiToAPI(emoji))),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import { statusToAPI } from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -58,17 +58,17 @@ export default apiRoute<{
|
|||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${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(
|
||||
await Promise.all(
|
||||
objects.map(async status => statusToAPI(status, user))
|
||||
objects.map(async (status) => statusToAPI(status, user)),
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
checkForBidirectionalRelationships,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
checkForBidirectionalRelationships,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -59,15 +59,15 @@ export default apiRoute<{
|
|||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${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(
|
||||
objects.map(user => userToAPI(user)),
|
||||
objects.map((user) => userToAPI(user)),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
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 manifest from "~package.json";
|
||||
import type { APIInstance } from "~types/entities/instance";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import type { MediaBackend } 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 { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||
|
||||
|
|
@ -52,12 +52,11 @@ export default apiRoute<{
|
|||
case "GET": {
|
||||
if (attachment.url) {
|
||||
return jsonResponse(attachmentToAPI(attachment));
|
||||
} else {
|
||||
}
|
||||
return new Response(null, {
|
||||
status: 206,
|
||||
});
|
||||
}
|
||||
}
|
||||
case "PUT": {
|
||||
const { description, thumbnail } = extraData.parsedRequest;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { encode } from "blurhash";
|
||||
import sharp from "sharp";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import { MediaBackendType } 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 { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ export default apiRoute<{
|
|||
if (file.size > config.validation.max_media_size) {
|
||||
return errorResponse(
|
||||
`File too large, max size is ${config.validation.max_media_size} bytes`,
|
||||
413
|
||||
413,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ export default apiRoute<{
|
|||
) {
|
||||
return errorResponse(
|
||||
`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?.height ?? 0,
|
||||
4,
|
||||
4
|
||||
4,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -33,5 +33,5 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(blocks.map(u => userToAPI(u)));
|
||||
return jsonResponse(blocks.map((u) => userToAPI(u)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { notificationToAPI } from "~database/entities/Notification";
|
||||
import {
|
||||
userRelations,
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -83,20 +83,20 @@ export default apiRoute<{
|
|||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`,
|
||||
);
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?since_id=${
|
||||
objects.at(-1)?.id
|
||||
}&limit=${limit}>; rel="prev"`
|
||||
}&limit=${limit}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map(n => notificationToAPI(n))),
|
||||
await Promise.all(objects.map((n) => notificationToAPI(n))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
return jsonResponse({
|
||||
ancestors: await Promise.all(
|
||||
ancestors.map(status => statusToAPI(status, user || undefined))
|
||||
ancestors.map((status) => statusToAPI(status, user || undefined)),
|
||||
),
|
||||
descendants: await Promise.all(
|
||||
descendants.map(status => statusToAPI(status, user || undefined))
|
||||
descendants.map((status) => statusToAPI(status, user || undefined)),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -85,20 +85,20 @@ export default apiRoute<{
|
|||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`,
|
||||
);
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?since_id=${
|
||||
objects[objects.length - 1].id
|
||||
}&limit=${limit}>; rel="prev"`
|
||||
}&limit=${limit}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
objects.map(user => userToAPI(user)),
|
||||
objects.map((user) => userToAPI(user)),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ export default apiRoute<{
|
|||
|
||||
if (req.method === "GET") {
|
||||
return jsonResponse(await statusToAPI(status));
|
||||
} else if (req.method === "DELETE") {
|
||||
}
|
||||
if (req.method === "DELETE") {
|
||||
if (status.authorId !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
|
@ -77,9 +78,10 @@ export default apiRoute<{
|
|||
// poll: Add source poll
|
||||
// media_attachments
|
||||
},
|
||||
200
|
||||
200,
|
||||
);
|
||||
} else if (req.method == "PUT") {
|
||||
}
|
||||
if (req.method === "PUT") {
|
||||
if (status.authorId !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
|
@ -99,7 +101,7 @@ export default apiRoute<{
|
|||
if (!statusText && !(media_ids && media_ids.length > 0)) {
|
||||
return errorResponse(
|
||||
"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) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_options}`,
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
options &&
|
||||
options.some(
|
||||
option => option.length > config.validation.max_poll_option_size
|
||||
options?.some(
|
||||
(option) =>
|
||||
option.length > config.validation.max_poll_option_size,
|
||||
)
|
||||
) {
|
||||
return errorResponse(
|
||||
`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) {
|
||||
return errorResponse(
|
||||
`Poll duration must be greater than ${config.validation.min_poll_duration} seconds`,
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (expires_in && expires_in > config.validation.max_poll_duration) {
|
||||
return errorResponse(
|
||||
`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) {
|
||||
return errorResponse(
|
||||
`Status must be less than ${config.validation.max_note_size} characters`,
|
||||
400
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if status body doesnt match filters
|
||||
if (
|
||||
config.filters.note_content.some(filter =>
|
||||
statusText?.match(filter)
|
||||
config.filters.note_content.some((filter) =>
|
||||
statusText?.match(filter),
|
||||
)
|
||||
) {
|
||||
return errorResponse("Status contains blocked words", 422);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
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";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -91,7 +91,7 @@ export default apiRoute<{
|
|||
...newReblog,
|
||||
uri: `${config.http.base_url}/statuses/${newReblog.id}`,
|
||||
},
|
||||
user
|
||||
)
|
||||
user,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -86,20 +86,20 @@ export default apiRoute<{
|
|||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`,
|
||||
);
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?since_id=${
|
||||
objects[objects.length - 1].id
|
||||
}&limit=${limit}>; rel="prev"`
|
||||
}&limit=${limit}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
objects.map(user => userToAPI(user)),
|
||||
objects.map((user) => userToAPI(user)),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export default apiRoute<{
|
|||
if (!status && !(media_ids && media_ids.length > 0)) {
|
||||
return errorResponse(
|
||||
"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) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_options}`,
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
options &&
|
||||
options.some(
|
||||
option => option.length > config.validation.max_poll_option_size
|
||||
options?.some(
|
||||
(option) => option.length > config.validation.max_poll_option_size,
|
||||
)
|
||||
) {
|
||||
return errorResponse(
|
||||
`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) {
|
||||
return errorResponse(
|
||||
`Poll duration must be greater than ${config.validation.min_poll_duration} seconds`,
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (expires_in && expires_in > config.validation.max_poll_duration) {
|
||||
return errorResponse(
|
||||
`Poll duration must be less than ${config.validation.max_poll_duration} seconds`,
|
||||
422
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -139,11 +138,11 @@ export default apiRoute<{
|
|||
let sanitizedStatus: string;
|
||||
|
||||
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") {
|
||||
// Parse as MFM
|
||||
// TODO: Parse as MFM
|
||||
sanitizedStatus = await sanitizeHtml(parse(status ?? "") as any);
|
||||
sanitizedStatus = await sanitizeHtml(parse(status ?? "") as string);
|
||||
} else {
|
||||
sanitizedStatus = await sanitizeHtml(status ?? "");
|
||||
}
|
||||
|
|
@ -151,7 +150,7 @@ export default apiRoute<{
|
|||
if (sanitizedStatus.length > config.validation.max_note_size) {
|
||||
return errorResponse(
|
||||
`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
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,17 +83,17 @@ export default apiRoute<{
|
|||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${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(
|
||||
await Promise.all(
|
||||
objects.map(async status => statusToAPI(status, user))
|
||||
objects.map(async (status) => statusToAPI(status, user)),
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,17 +72,19 @@ export default apiRoute<{
|
|||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${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(
|
||||
await Promise.all(
|
||||
objects.map(async status => statusToAPI(status, user || undefined))
|
||||
objects.map(async (status) =>
|
||||
statusToAPI(status, user || undefined),
|
||||
),
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { encode } from "blurhash";
|
||||
import sharp from "sharp";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import type { MediaBackend } 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 { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ export default apiRoute<{
|
|||
if (file.size > config.validation.max_media_size) {
|
||||
return errorResponse(
|
||||
`File too large, max size is ${config.validation.max_media_size} bytes`,
|
||||
413
|
||||
413,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ export default apiRoute<{
|
|||
) {
|
||||
return errorResponse(
|
||||
`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?.height ?? 0,
|
||||
4,
|
||||
4
|
||||
4,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
|
@ -136,13 +136,13 @@ export default apiRoute<{
|
|||
|
||||
if (isImage) {
|
||||
return jsonResponse(attachmentToAPI(newAttachment));
|
||||
} else {
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
...attachmentToAPI(newAttachment),
|
||||
url: null,
|
||||
},
|
||||
202
|
||||
202,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export default apiRoute<{
|
|||
if (!user && (resolve || offset)) {
|
||||
return errorResponse(
|
||||
"Cannot use resolve or offset without being authenticated",
|
||||
401
|
||||
401,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ export default apiRoute<{
|
|||
const accounts = await client.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: accountResults.map(hit => hit.id),
|
||||
in: accountResults.map((hit) => hit.id),
|
||||
},
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
|
|
@ -115,7 +115,7 @@ export default apiRoute<{
|
|||
const statuses = await client.status.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: statusResults.map(hit => hit.id),
|
||||
in: statusResults.map((hit) => hit.id),
|
||||
},
|
||||
author: {
|
||||
relationshipSubjects: {
|
||||
|
|
@ -134,9 +134,9 @@ export default apiRoute<{
|
|||
});
|
||||
|
||||
return jsonResponse({
|
||||
accounts: accounts.map(account => userToAPI(account)),
|
||||
accounts: accounts.map((account) => userToAPI(account)),
|
||||
statuses: await Promise.all(
|
||||
statuses.map(status => statusToAPI(status))
|
||||
statuses.map((status) => statusToAPI(status)),
|
||||
),
|
||||
hashtags: [],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { randomBytes } from "node:crypto";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { randomBytes } from "crypto";
|
||||
import { client } from "~database/datasource";
|
||||
import { TokenType } from "~database/entities/Token";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
|
@ -34,12 +34,11 @@ export default apiRoute<{
|
|||
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?` +
|
||||
new URLSearchParams({
|
||||
`/oauth/authorize?${new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString(),
|
||||
302
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
|
||||
if (response_type !== "code")
|
||||
|
|
@ -91,15 +90,14 @@ export default apiRoute<{
|
|||
|
||||
// Redirect to OAuth confirmation screen
|
||||
return Response.redirect(
|
||||
`/oauth/redirect?` +
|
||||
new URLSearchParams({
|
||||
`/oauth/redirect?${new URLSearchParams({
|
||||
redirect_uri,
|
||||
code,
|
||||
client_id,
|
||||
application: application.name,
|
||||
website: application.website ?? "",
|
||||
scope: scopes.join(" "),
|
||||
}).toString(),
|
||||
302
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,12 +27,11 @@ export default apiRoute<{
|
|||
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?` +
|
||||
new URLSearchParams({
|
||||
`/oauth/authorize?${new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString(),
|
||||
302
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
|
||||
// Get token
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { errorResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -19,7 +19,7 @@ export default apiRoute(async (req, matchedRoute) => {
|
|||
const id = matchedRoute.params.id;
|
||||
|
||||
// parse `Range` header
|
||||
const [start = 0, end = Infinity] = (
|
||||
const [start = 0, end = Number.POSITIVE_INFINITY] = (
|
||||
(req.headers.get("Range") || "")
|
||||
.split("=") // ["Range: bytes", "0-100"]
|
||||
.at(-1) || ""
|
||||
|
|
|
|||
|
|
@ -26,12 +26,11 @@ export const meta = applyConfig({
|
|||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?` +
|
||||
new URLSearchParams({
|
||||
`/oauth/authorize?${new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString(),
|
||||
302
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
|
||||
const issuerId = matchedRoute.query.issuer;
|
||||
|
|
@ -46,7 +45,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
const issuer = config.oidc.providers.find(
|
||||
provider => provider.id === issuerId
|
||||
(provider) => provider.id === issuerId,
|
||||
);
|
||||
|
||||
if (!issuer) {
|
||||
|
|
@ -57,7 +56,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
const authServer = await discoveryRequest(issuerUrl, {
|
||||
algorithm: "oidc",
|
||||
}).then(res => processDiscoveryResponse(issuerUrl, res));
|
||||
}).then((res) => processDiscoveryResponse(issuerUrl, res));
|
||||
|
||||
const codeVerifier = generateRandomCodeVerifier();
|
||||
|
||||
|
|
@ -78,18 +77,15 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
||||
|
||||
return Response.redirect(
|
||||
authServer.authorization_endpoint +
|
||||
"?" +
|
||||
new URLSearchParams({
|
||||
`${authServer.authorization_endpoint}?${new URLSearchParams({
|
||||
client_id: issuer.client_id,
|
||||
redirect_uri:
|
||||
oauthRedirectUri(issuerId) + `?flow=${newFlow.id}`,
|
||||
redirect_uri: `${oauthRedirectUri(issuerId)}?flow=${newFlow.id}`,
|
||||
response_type: "code",
|
||||
scope: "openid profile email",
|
||||
// PKCE
|
||||
code_challenge_method: "S256",
|
||||
code_challenge: codeChallenge,
|
||||
}).toString(),
|
||||
302
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue