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
|
|
@ -1,22 +1,22 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/strict-type-checked",
|
"plugin:@typescript-eslint/strict-type-checked",
|
||||||
"plugin:@typescript-eslint/stylistic",
|
"plugin:@typescript-eslint/stylistic",
|
||||||
"plugin:prettier/recommended",
|
"plugin:prettier/recommended",
|
||||||
],
|
],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: "./tsconfig.json",
|
project: "./tsconfig.json",
|
||||||
},
|
},
|
||||||
ignorePatterns: ["node_modules/", "dist/", ".eslintrc.cjs"],
|
ignorePatterns: ["node_modules/", "dist/", ".eslintrc.cjs"],
|
||||||
plugins: ["@typescript-eslint"],
|
plugins: ["@typescript-eslint"],
|
||||||
root: true,
|
root: true,
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
"@typescript-eslint/no-unsafe-argument": "off",
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/consistent-type-exports": "error",
|
"@typescript-eslint/consistent-type-exports": "error",
|
||||||
"@typescript-eslint/consistent-type-imports": "error"
|
"@typescript-eslint/consistent-type-imports": "error",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
|
|
@ -4,10 +4,7 @@
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"name": "vscode-jest-tests.v2.lysand",
|
"name": "vscode-jest-tests.v2.lysand",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"args": [
|
"args": ["test", "${jest.testFile}"],
|
||||||
"test",
|
|
||||||
"${jest.testFile}"
|
|
||||||
],
|
|
||||||
"cwd": "/home/jessew/Dev/lysand",
|
"cwd": "/home/jessew/Dev/lysand",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"internalConsoleOptions": "neverOpen",
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
|
@ -15,4 +12,4 @@
|
||||||
"program": "/home/jessew/.bun/bin/bun"
|
"program": "/home/jessew/.bun/bin/bun"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"jest.jestCommandLine": "/home/jessew/.bun/bin/bun test",
|
"jest.jestCommandLine": "/home/jessew/.bun/bin/bun test",
|
||||||
"jest.rootPath": "."
|
"jest.rootPath": "."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ const requests: Promise<Response>[] = [];
|
||||||
|
|
||||||
// Repeat 1000 times
|
// Repeat 1000 times
|
||||||
for (let i = 0; i < 1000; i++) {
|
for (let i = 0; i < 1000; i++) {
|
||||||
requests.push(
|
requests.push(
|
||||||
fetch(`https://mastodon.social`, {
|
fetch("https://mastodon.social", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(requests);
|
await Promise.all(requests);
|
||||||
|
|
|
||||||
|
|
@ -9,46 +9,46 @@ const token = process.env.TOKEN;
|
||||||
const requestCount = Number(process.argv[2]) || 100;
|
const requestCount = Number(process.argv[2]) || 100;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.red(
|
`${chalk.red(
|
||||||
"✗"
|
"✗",
|
||||||
)} No token provided. Provide one via the TOKEN environment variable.`
|
)} No token provided. Provide one via the TOKEN environment variable.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTimeline = () =>
|
const fetchTimeline = () =>
|
||||||
fetch(`${config.http.base_url}/api/v1/timelines/home`, {
|
fetch(`${config.http.base_url}/api/v1/timelines/home`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
}).then(res => res.ok);
|
}).then((res) => res.ok);
|
||||||
|
|
||||||
const timeNow = performance.now();
|
const timeNow = performance.now();
|
||||||
|
|
||||||
const requests = Array.from({ length: requestCount }, () => fetchTimeline());
|
const requests = Array.from({ length: requestCount }, () => fetchTimeline());
|
||||||
|
|
||||||
Promise.all(requests)
|
Promise.all(requests)
|
||||||
.then(results => {
|
.then((results) => {
|
||||||
const timeTaken = performance.now() - timeNow;
|
const timeTaken = performance.now() - timeNow;
|
||||||
if (results.every(t => t)) {
|
if (results.every((t) => t)) {
|
||||||
console.log(`${chalk.green("✓")} All requests succeeded`);
|
console.log(`${chalk.green("✓")} All requests succeeded`);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.red("✗")} ${
|
`${chalk.red("✗")} ${
|
||||||
results.filter(t => !t).length
|
results.filter((t) => !t).length
|
||||||
} requests failed`
|
} requests failed`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.green("✓")} ${
|
`${chalk.green("✓")} ${
|
||||||
requests.length
|
requests.length
|
||||||
} requests fulfilled in ${chalk.bold(
|
} requests fulfilled in ${chalk.bold(
|
||||||
(timeTaken / 1000).toFixed(5)
|
(timeTaken / 1000).toFixed(5),
|
||||||
)}s`
|
)}s`,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.log(`${chalk.red("✗")} ${err}`);
|
console.log(`${chalk.red("✗")} ${err}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
33
biome.json
33
biome.json
|
|
@ -1,17 +1,20 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
},
|
"ignore": ["node_modules/**/*", "dist/**/*"]
|
||||||
"linter": {
|
},
|
||||||
"enabled": true,
|
"linter": {
|
||||||
"rules": {
|
"enabled": true,
|
||||||
"recommended": true
|
"rules": {
|
||||||
}
|
"recommended": true
|
||||||
},
|
},
|
||||||
"formatter": {
|
"ignore": ["node_modules/**/*", "dist/**/*"]
|
||||||
"enabled": true,
|
},
|
||||||
"indentStyle": "space",
|
"formatter": {
|
||||||
"indentWidth": 4
|
"enabled": true,
|
||||||
}
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 4,
|
||||||
|
"ignore": ["node_modules/**/*", "dist/**/*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
build.ts
52
build.ts
|
|
@ -1,52 +1,52 @@
|
||||||
// Delete dist directory
|
// Delete dist directory
|
||||||
import { rm, cp, mkdir, exists } from "fs/promises";
|
import { cp, exists, mkdir, rm } from "node:fs/promises";
|
||||||
import { rawRoutes } from "~routes";
|
import { rawRoutes } from "~routes";
|
||||||
|
|
||||||
if (!(await exists("./pages/dist"))) {
|
if (!(await exists("./pages/dist"))) {
|
||||||
console.log("Please build the Vite server first, or use `bun prod-build`");
|
console.log("Please build the Vite server first, or use `bun prod-build`");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Building at ${process.cwd()}`);
|
console.log(`Building at ${process.cwd()}`);
|
||||||
|
|
||||||
await rm("./dist", { recursive: true });
|
await rm("./dist", { recursive: true });
|
||||||
|
|
||||||
await mkdir(process.cwd() + "/dist");
|
await mkdir(`${process.cwd()}/dist`);
|
||||||
|
|
||||||
//bun build --entrypoints ./index.ts ./prisma.ts ./cli.ts --outdir dist --target bun --splitting --minify --external bullmq,@prisma/client
|
//bun build --entrypoints ./index.ts ./prisma.ts ./cli.ts --outdir dist --target bun --splitting --minify --external bullmq,@prisma/client
|
||||||
await Bun.build({
|
await Bun.build({
|
||||||
entrypoints: [
|
entrypoints: [
|
||||||
process.cwd() + "/index.ts",
|
`${process.cwd()}/index.ts`,
|
||||||
process.cwd() + "/prisma.ts",
|
`${process.cwd()}/prisma.ts`,
|
||||||
process.cwd() + "/cli.ts",
|
`${process.cwd()}/cli.ts`,
|
||||||
// Force Bun to include endpoints
|
// Force Bun to include endpoints
|
||||||
...Object.values(rawRoutes),
|
...Object.values(rawRoutes),
|
||||||
],
|
],
|
||||||
outdir: process.cwd() + "/dist",
|
outdir: `${process.cwd()}/dist`,
|
||||||
target: "bun",
|
target: "bun",
|
||||||
splitting: true,
|
splitting: true,
|
||||||
minify: true,
|
minify: true,
|
||||||
external: ["bullmq"],
|
external: ["bullmq"],
|
||||||
}).then(output => {
|
}).then((output) => {
|
||||||
if (!output.success) {
|
if (!output.success) {
|
||||||
console.log(output.logs);
|
console.log(output.logs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create pages directory
|
// Create pages directory
|
||||||
// mkdir ./dist/pages
|
// mkdir ./dist/pages
|
||||||
await mkdir(process.cwd() + "/dist/pages");
|
await mkdir(`${process.cwd()}/dist/pages`);
|
||||||
|
|
||||||
// Copy Vite build output to dist
|
// Copy Vite build output to dist
|
||||||
// cp -r ./pages/dist ./dist/pages
|
// cp -r ./pages/dist ./dist/pages
|
||||||
await cp(process.cwd() + "/pages/dist", process.cwd() + "/dist/pages/", {
|
await cp(`${process.cwd()}/pages/dist`, `${process.cwd()}/dist/pages/`, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy the Bee Movie script from pages
|
// Copy the Bee Movie script from pages
|
||||||
await cp(
|
await cp(
|
||||||
process.cwd() + "/pages/beemovie.txt",
|
`${process.cwd()}/pages/beemovie.txt`,
|
||||||
process.cwd() + "/dist/pages/beemovie.txt"
|
`${process.cwd()}/dist/pages/beemovie.txt`,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Built!`);
|
console.log("Built!");
|
||||||
|
|
|
||||||
BIN
bun.lockb
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { PrismaClient } from "@prisma/client";
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
|
|
||||||
const client = new PrismaClient({
|
const client = new PrismaClient({
|
||||||
datasourceUrl: `postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}`,
|
datasourceUrl: `postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
/* const federationQueue = new Queue("federation", {
|
/* const federationQueue = new Queue("federation", {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { APIApplication } from "~types/entities/application";
|
|
||||||
import type { Application } from "@prisma/client";
|
import type { Application } from "@prisma/client";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import type { APIApplication } from "~types/entities/application";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an application that can authenticate with the API.
|
* Represents an application that can authenticate with the API.
|
||||||
|
|
@ -12,18 +12,18 @@ import { client } from "~database/datasource";
|
||||||
* @returns The application associated with the given access token, or null if no such application exists.
|
* @returns The application associated with the given access token, or null if no such application exists.
|
||||||
*/
|
*/
|
||||||
export const getFromToken = async (
|
export const getFromToken = async (
|
||||||
token: string
|
token: string,
|
||||||
): Promise<Application | null> => {
|
): Promise<Application | null> => {
|
||||||
const dbToken = await client.token.findFirst({
|
const dbToken = await client.token.findFirst({
|
||||||
where: {
|
where: {
|
||||||
access_token: token,
|
access_token: token,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
application: true,
|
application: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return dbToken?.application || null;
|
return dbToken?.application || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,9 +31,9 @@ export const getFromToken = async (
|
||||||
* @returns The API application representation of this application.
|
* @returns The API application representation of this application.
|
||||||
*/
|
*/
|
||||||
export const applicationToAPI = (app: Application): APIApplication => {
|
export const applicationToAPI = (app: Application): APIApplication => {
|
||||||
return {
|
return {
|
||||||
name: app.name,
|
name: app.name,
|
||||||
website: app.website,
|
website: app.website,
|
||||||
vapid_key: app.vapid_key,
|
vapid_key: app.vapid_key,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,70 @@
|
||||||
import type { Attachment } from "@prisma/client";
|
import type { Attachment } from "@prisma/client";
|
||||||
import type { ConfigType } from "config-manager";
|
import type { Config } from "config-manager";
|
||||||
import { MediaBackendType } from "media-manager";
|
import { MediaBackendType } from "media-manager";
|
||||||
import type { APIAsyncAttachment } from "~types/entities/async_attachment";
|
import type { APIAsyncAttachment } from "~types/entities/async_attachment";
|
||||||
import type { APIAttachment } from "~types/entities/attachment";
|
import type { APIAttachment } from "~types/entities/attachment";
|
||||||
|
|
||||||
export const attachmentToAPI = (
|
export const attachmentToAPI = (
|
||||||
attachment: Attachment
|
attachment: Attachment,
|
||||||
): APIAsyncAttachment | APIAttachment => {
|
): APIAsyncAttachment | APIAttachment => {
|
||||||
let type = "unknown";
|
let type = "unknown";
|
||||||
|
|
||||||
if (attachment.mime_type.startsWith("image/")) {
|
if (attachment.mime_type.startsWith("image/")) {
|
||||||
type = "image";
|
type = "image";
|
||||||
} else if (attachment.mime_type.startsWith("video/")) {
|
} else if (attachment.mime_type.startsWith("video/")) {
|
||||||
type = "video";
|
type = "video";
|
||||||
} else if (attachment.mime_type.startsWith("audio/")) {
|
} else if (attachment.mime_type.startsWith("audio/")) {
|
||||||
type = "audio";
|
type = "audio";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: attachment.id,
|
id: attachment.id,
|
||||||
type: type as any,
|
type: type as "image" | "video" | "audio" | "unknown",
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
remote_url: attachment.remote_url,
|
remote_url: attachment.remote_url,
|
||||||
preview_url: attachment.thumbnail_url,
|
preview_url: attachment.thumbnail_url,
|
||||||
text_url: null,
|
text_url: null,
|
||||||
meta: {
|
meta: {
|
||||||
width: attachment.width || undefined,
|
width: attachment.width || undefined,
|
||||||
height: attachment.height || undefined,
|
height: attachment.height || undefined,
|
||||||
fps: attachment.fps || undefined,
|
fps: attachment.fps || undefined,
|
||||||
size:
|
size:
|
||||||
attachment.width && attachment.height
|
attachment.width && attachment.height
|
||||||
? `${attachment.width}x${attachment.height}`
|
? `${attachment.width}x${attachment.height}`
|
||||||
: undefined,
|
: undefined,
|
||||||
duration: attachment.duration || undefined,
|
duration: attachment.duration || undefined,
|
||||||
length: attachment.size?.toString() || undefined,
|
length: attachment.size?.toString() || undefined,
|
||||||
aspect:
|
aspect:
|
||||||
attachment.width && attachment.height
|
attachment.width && attachment.height
|
||||||
? attachment.width / attachment.height
|
? attachment.width / attachment.height
|
||||||
: undefined,
|
: undefined,
|
||||||
original: {
|
original: {
|
||||||
width: attachment.width || undefined,
|
width: attachment.width || undefined,
|
||||||
height: attachment.height || undefined,
|
height: attachment.height || undefined,
|
||||||
size:
|
size:
|
||||||
attachment.width && attachment.height
|
attachment.width && attachment.height
|
||||||
? `${attachment.width}x${attachment.height}`
|
? `${attachment.width}x${attachment.height}`
|
||||||
: undefined,
|
: undefined,
|
||||||
aspect:
|
aspect:
|
||||||
attachment.width && attachment.height
|
attachment.width && attachment.height
|
||||||
? attachment.width / attachment.height
|
? attachment.width / attachment.height
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
// Idk whether size or length is the right value
|
// Idk whether size or length is the right value
|
||||||
},
|
},
|
||||||
description: attachment.description,
|
description: attachment.description,
|
||||||
blurhash: attachment.blurhash,
|
blurhash: attachment.blurhash,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUrl = (name: string, config: ConfigType) => {
|
export const getUrl = (name: string, config: Config) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||||
return `${config.http.base_url}/media/${name}`;
|
return `${config.http.base_url}/media/${name}`;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison, @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison, @typescript-eslint/no-unnecessary-condition
|
||||||
} else if (config.media.backend === MediaBackendType.S3) {
|
}
|
||||||
return `${config.s3.public_url}/${name}`;
|
if (config.media.backend === MediaBackendType.S3) {
|
||||||
}
|
return `${config.s3.public_url}/${name}`;
|
||||||
return "";
|
}
|
||||||
|
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 { APIEmoji } from "~types/entities/emoji";
|
||||||
import type { Emoji as LysandEmoji } from "~types/lysand/extensions/org.lysand/custom_emojis";
|
import type { Emoji as LysandEmoji } from "~types/lysand/extensions/org.lysand/custom_emojis";
|
||||||
import { client } from "~database/datasource";
|
|
||||||
import type { Emoji } from "@prisma/client";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an emoji entity in the database.
|
* Represents an emoji entity in the database.
|
||||||
|
|
@ -13,41 +13,41 @@ import type { Emoji } from "@prisma/client";
|
||||||
* @returns An array of emojis
|
* @returns An array of emojis
|
||||||
*/
|
*/
|
||||||
export const parseEmojis = async (text: string): Promise<Emoji[]> => {
|
export const parseEmojis = async (text: string): Promise<Emoji[]> => {
|
||||||
const regex = /:[a-zA-Z0-9_]+:/g;
|
const regex = /:[a-zA-Z0-9_]+:/g;
|
||||||
const matches = text.match(regex);
|
const matches = text.match(regex);
|
||||||
if (!matches) return [];
|
if (!matches) return [];
|
||||||
return await client.emoji.findMany({
|
return await client.emoji.findMany({
|
||||||
where: {
|
where: {
|
||||||
shortcode: {
|
shortcode: {
|
||||||
in: matches.map(match => match.replace(/:/g, "")),
|
in: matches.map((match) => match.replace(/:/g, "")),
|
||||||
},
|
},
|
||||||
instanceId: null,
|
instanceId: null,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
instance: true,
|
instance: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addEmojiIfNotExists = async (emoji: LysandEmoji) => {
|
export const addEmojiIfNotExists = async (emoji: LysandEmoji) => {
|
||||||
const existingEmoji = await client.emoji.findFirst({
|
const existingEmoji = await client.emoji.findFirst({
|
||||||
where: {
|
where: {
|
||||||
shortcode: emoji.name,
|
shortcode: emoji.name,
|
||||||
instance: null,
|
instance: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingEmoji) return existingEmoji;
|
if (existingEmoji) return existingEmoji;
|
||||||
|
|
||||||
return await client.emoji.create({
|
return await client.emoji.create({
|
||||||
data: {
|
data: {
|
||||||
shortcode: emoji.name,
|
shortcode: emoji.name,
|
||||||
url: emoji.url[0].content,
|
url: emoji.url[0].content,
|
||||||
alt: emoji.alt || null,
|
alt: emoji.alt || null,
|
||||||
content_type: emoji.url[0].content_type,
|
content_type: emoji.url[0].content_type,
|
||||||
visible_in_picker: true,
|
visible_in_picker: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,43 +55,43 @@ export const addEmojiIfNotExists = async (emoji: LysandEmoji) => {
|
||||||
* @returns The APIEmoji object.
|
* @returns The APIEmoji object.
|
||||||
*/
|
*/
|
||||||
export const emojiToAPI = (emoji: Emoji): APIEmoji => {
|
export const emojiToAPI = (emoji: Emoji): APIEmoji => {
|
||||||
return {
|
return {
|
||||||
shortcode: emoji.shortcode,
|
shortcode: emoji.shortcode,
|
||||||
static_url: emoji.url, // TODO: Add static version
|
static_url: emoji.url, // TODO: Add static version
|
||||||
url: emoji.url,
|
url: emoji.url,
|
||||||
visible_in_picker: emoji.visible_in_picker,
|
visible_in_picker: emoji.visible_in_picker,
|
||||||
category: undefined,
|
category: undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const emojiToLysand = (emoji: Emoji): LysandEmoji => {
|
export const emojiToLysand = (emoji: Emoji): LysandEmoji => {
|
||||||
return {
|
return {
|
||||||
name: emoji.shortcode,
|
name: emoji.shortcode,
|
||||||
url: [
|
url: [
|
||||||
{
|
{
|
||||||
content: emoji.url,
|
content: emoji.url,
|
||||||
content_type: emoji.content_type,
|
content_type: emoji.content_type,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
alt: emoji.alt || undefined,
|
alt: emoji.alt || undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the emoji to an ActivityPub object.
|
* Converts the emoji to an ActivityPub object.
|
||||||
* @returns The ActivityPub object.
|
* @returns The ActivityPub object.
|
||||||
*/
|
*/
|
||||||
export const emojiToActivityPub = (emoji: Emoji): any => {
|
export const emojiToActivityPub = (emoji: Emoji): object => {
|
||||||
// replace any with your ActivityPub Emoji type
|
// replace any with your ActivityPub Emoji type
|
||||||
return {
|
return {
|
||||||
type: "Emoji",
|
type: "Emoji",
|
||||||
name: `:${emoji.shortcode}:`,
|
name: `:${emoji.shortcode}:`,
|
||||||
updated: new Date().toISOString(),
|
updated: new Date().toISOString(),
|
||||||
icon: {
|
icon: {
|
||||||
type: "Image",
|
type: "Image",
|
||||||
url: emoji.url,
|
url: emoji.url,
|
||||||
mediaType: emoji.content_type,
|
mediaType: emoji.content_type,
|
||||||
alt: emoji.alt || undefined,
|
alt: emoji.alt || undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,38 +12,38 @@ import type { ServerMetadata } from "~types/lysand/Object";
|
||||||
* @returns Either the database instance if it already exists, or a newly created instance.
|
* @returns Either the database instance if it already exists, or a newly created instance.
|
||||||
*/
|
*/
|
||||||
export const addInstanceIfNotExists = async (
|
export const addInstanceIfNotExists = async (
|
||||||
url: string
|
url: string,
|
||||||
): Promise<Instance> => {
|
): Promise<Instance> => {
|
||||||
const origin = new URL(url).origin;
|
const origin = new URL(url).origin;
|
||||||
const hostname = new URL(url).hostname;
|
const hostname = new URL(url).hostname;
|
||||||
|
|
||||||
const found = await client.instance.findFirst({
|
const found = await client.instance.findFirst({
|
||||||
where: {
|
where: {
|
||||||
base_url: hostname,
|
base_url: hostname,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
|
|
||||||
// Fetch the instance configuration
|
// Fetch the instance configuration
|
||||||
const metadata = (await fetch(`${origin}/.well-known/lysand`).then(res =>
|
const metadata = (await fetch(`${origin}/.well-known/lysand`).then((res) =>
|
||||||
res.json()
|
res.json(),
|
||||||
)) as Partial<ServerMetadata>;
|
)) as Partial<ServerMetadata>;
|
||||||
|
|
||||||
if (metadata.type !== "ServerMetadata") {
|
if (metadata.type !== "ServerMetadata") {
|
||||||
throw new Error("Invalid instance metadata");
|
throw new Error("Invalid instance metadata");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(metadata.name && metadata.version)) {
|
if (!(metadata.name && metadata.version)) {
|
||||||
throw new Error("Invalid instance metadata");
|
throw new Error("Invalid instance metadata");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await client.instance.create({
|
return await client.instance.create({
|
||||||
data: {
|
data: {
|
||||||
base_url: hostname,
|
base_url: hostname,
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
version: metadata.version,
|
version: metadata.version,
|
||||||
logo: metadata.logo as any,
|
logo: metadata.logo,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
|
import type { Like, Prisma } from "@prisma/client";
|
||||||
|
import { config } from "config-manager";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import type { Like as LysandLike } from "~types/lysand/Object";
|
import type { Like as LysandLike } from "~types/lysand/Object";
|
||||||
import type { Like } from "@prisma/client";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
import type { UserWithRelations } from "./User";
|
|
||||||
import type { StatusWithRelations } from "./Status";
|
import type { StatusWithRelations } from "./Status";
|
||||||
import { config } from "config-manager";
|
import type { UserWithRelations } from "./User";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Like entity in the database.
|
* Represents a Like entity in the database.
|
||||||
*/
|
*/
|
||||||
export const toLysand = (like: Like): LysandLike => {
|
export const toLysand = (like: Like): LysandLike => {
|
||||||
return {
|
return {
|
||||||
id: like.id,
|
id: like.id,
|
||||||
author: (like as any).liker?.uri,
|
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||||
type: "Like",
|
author: (like as any).liker?.uri,
|
||||||
created_at: new Date(like.createdAt).toISOString(),
|
type: "Like",
|
||||||
object: (like as any).liked?.uri,
|
created_at: new Date(like.createdAt).toISOString(),
|
||||||
uri: `${config.http.base_url}/actions/${like.id}`,
|
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||||
};
|
object: (like as any).liked?.uri,
|
||||||
|
uri: `${config.http.base_url}/actions/${like.id}`,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,29 +28,29 @@ export const toLysand = (like: Like): LysandLike => {
|
||||||
* @param status Status being liked
|
* @param status Status being liked
|
||||||
*/
|
*/
|
||||||
export const createLike = async (
|
export const createLike = async (
|
||||||
user: UserWithRelations,
|
user: UserWithRelations,
|
||||||
status: StatusWithRelations
|
status: StatusWithRelations,
|
||||||
) => {
|
) => {
|
||||||
await client.like.create({
|
await client.like.create({
|
||||||
data: {
|
data: {
|
||||||
likedId: status.id,
|
likedId: status.id,
|
||||||
likerId: user.id,
|
likerId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status.author.instanceId === user.instanceId) {
|
if (status.author.instanceId === user.instanceId) {
|
||||||
// Notify the user that their post has been favourited
|
// Notify the user that their post has been favourited
|
||||||
await client.notification.create({
|
await client.notification.create({
|
||||||
data: {
|
data: {
|
||||||
accountId: user.id,
|
accountId: user.id,
|
||||||
type: "favourite",
|
type: "favourite",
|
||||||
notifiedId: status.authorId,
|
notifiedId: status.authorId,
|
||||||
statusId: status.id,
|
statusId: status.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// TODO: Add database jobs for federating this
|
// TODO: Add database jobs for federating this
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,28 +59,28 @@ export const createLike = async (
|
||||||
* @param status Status being unliked
|
* @param status Status being unliked
|
||||||
*/
|
*/
|
||||||
export const deleteLike = async (
|
export const deleteLike = async (
|
||||||
user: UserWithRelations,
|
user: UserWithRelations,
|
||||||
status: StatusWithRelations
|
status: StatusWithRelations,
|
||||||
) => {
|
) => {
|
||||||
await client.like.deleteMany({
|
await client.like.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
likedId: status.id,
|
likedId: status.id,
|
||||||
likerId: user.id,
|
likerId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify the user that their post has been favourited
|
// Notify the user that their post has been favourited
|
||||||
await client.notification.deleteMany({
|
await client.notification.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
accountId: user.id,
|
accountId: user.id,
|
||||||
type: "favourite",
|
type: "favourite",
|
||||||
notifiedId: status.authorId,
|
notifiedId: status.authorId,
|
||||||
statusId: status.id,
|
statusId: status.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user.instanceId === null && status.author.instanceId !== null) {
|
if (user.instanceId === null && status.author.instanceId !== null) {
|
||||||
// User is local, federate the delete
|
// User is local, federate the delete
|
||||||
// TODO: Federate this
|
// TODO: Federate this
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,20 @@ import { type StatusWithRelations, statusToAPI } from "./Status";
|
||||||
import { type UserWithRelations, userToAPI } from "./User";
|
import { type UserWithRelations, userToAPI } from "./User";
|
||||||
|
|
||||||
export type NotificationWithRelations = Notification & {
|
export type NotificationWithRelations = Notification & {
|
||||||
status: StatusWithRelations | null;
|
status: StatusWithRelations | null;
|
||||||
account: UserWithRelations;
|
account: UserWithRelations;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const notificationToAPI = async (
|
export const notificationToAPI = async (
|
||||||
notification: NotificationWithRelations
|
notification: NotificationWithRelations,
|
||||||
): Promise<APINotification> => {
|
): Promise<APINotification> => {
|
||||||
return {
|
return {
|
||||||
account: userToAPI(notification.account),
|
account: userToAPI(notification.account),
|
||||||
created_at: new Date(notification.createdAt).toISOString(),
|
created_at: new Date(notification.createdAt).toISOString(),
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
status: notification.status
|
status: notification.status
|
||||||
? await statusToAPI(notification.status, notification.account)
|
? await statusToAPI(notification.status, notification.account)
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,79 +9,79 @@ import type { LysandObjectType } from "~types/lysand/Object";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const createFromObject = async (object: LysandObjectType) => {
|
export const createFromObject = async (object: LysandObjectType) => {
|
||||||
const foundObject = await client.lysandObject.findFirst({
|
const foundObject = await client.lysandObject.findFirst({
|
||||||
where: { remote_id: object.id },
|
where: { remote_id: object.id },
|
||||||
include: {
|
include: {
|
||||||
author: true,
|
author: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (foundObject) {
|
if (foundObject) {
|
||||||
return foundObject;
|
return foundObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
const author = await client.lysandObject.findFirst({
|
const author = await client.lysandObject.findFirst({
|
||||||
where: { uri: (object as any).author },
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
});
|
where: { uri: (object as any).author },
|
||||||
|
});
|
||||||
|
|
||||||
return await client.lysandObject.create({
|
return await client.lysandObject.create({
|
||||||
data: {
|
data: {
|
||||||
authorId: author?.id,
|
authorId: author?.id,
|
||||||
created_at: new Date(object.created_at),
|
created_at: new Date(object.created_at),
|
||||||
extensions: object.extensions || {},
|
extensions: object.extensions || {},
|
||||||
remote_id: object.id,
|
remote_id: object.id,
|
||||||
type: object.type,
|
type: object.type,
|
||||||
uri: object.uri,
|
uri: object.uri,
|
||||||
// Rest of data (remove id, author, created_at, extensions, type, uri)
|
// Rest of data (remove id, author, created_at, extensions, type, uri)
|
||||||
extra_data: Object.fromEntries(
|
extra_data: Object.fromEntries(
|
||||||
Object.entries(object).filter(
|
Object.entries(object).filter(
|
||||||
([key]) =>
|
([key]) =>
|
||||||
![
|
![
|
||||||
"id",
|
"id",
|
||||||
"author",
|
"author",
|
||||||
"created_at",
|
"created_at",
|
||||||
"extensions",
|
"extensions",
|
||||||
"type",
|
"type",
|
||||||
"uri",
|
"uri",
|
||||||
].includes(key)
|
].includes(key),
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toLysand = (lyObject: LysandObject): LysandObjectType => {
|
export const toLysand = (lyObject: LysandObject): LysandObjectType => {
|
||||||
return {
|
return {
|
||||||
id: lyObject.remote_id || lyObject.id,
|
id: lyObject.remote_id || lyObject.id,
|
||||||
created_at: new Date(lyObject.created_at).toISOString(),
|
created_at: new Date(lyObject.created_at).toISOString(),
|
||||||
type: lyObject.type,
|
type: lyObject.type,
|
||||||
uri: lyObject.uri,
|
uri: lyObject.uri,
|
||||||
// @ts-expect-error This works, I promise
|
...lyObject.extra_data,
|
||||||
...lyObject.extra_data,
|
extensions: lyObject.extensions,
|
||||||
extensions: lyObject.extensions,
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPublication = (lyObject: LysandObject): boolean => {
|
export const isPublication = (lyObject: LysandObject): boolean => {
|
||||||
return lyObject.type === "Note" || lyObject.type === "Patch";
|
return lyObject.type === "Note" || lyObject.type === "Patch";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAction = (lyObject: LysandObject): boolean => {
|
export const isAction = (lyObject: LysandObject): boolean => {
|
||||||
return [
|
return [
|
||||||
"Like",
|
"Like",
|
||||||
"Follow",
|
"Follow",
|
||||||
"Dislike",
|
"Dislike",
|
||||||
"FollowAccept",
|
"FollowAccept",
|
||||||
"FollowReject",
|
"FollowReject",
|
||||||
"Undo",
|
"Undo",
|
||||||
"Announce",
|
"Announce",
|
||||||
].includes(lyObject.type);
|
].includes(lyObject.type);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isActor = (lyObject: LysandObject): boolean => {
|
export const isActor = (lyObject: LysandObject): boolean => {
|
||||||
return lyObject.type === "User";
|
return lyObject.type === "User";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isExtension = (lyObject: LysandObject): boolean => {
|
export const isExtension = (lyObject: LysandObject): boolean => {
|
||||||
return lyObject.type === "Extension";
|
return lyObject.type === "Extension";
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// import { Worker } from "bullmq";
|
|
||||||
import { statusToLysand, type StatusWithRelations } from "./Status";
|
|
||||||
import type { User } from "@prisma/client";
|
import type { User } from "@prisma/client";
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
|
// import { Worker } from "bullmq";
|
||||||
|
import { type StatusWithRelations, statusToLysand } from "./Status";
|
||||||
|
|
||||||
/* export const federationWorker = new Worker(
|
/* export const federationWorker = new Worker(
|
||||||
"federation",
|
"federation",
|
||||||
|
|
@ -123,68 +123,68 @@ import { config } from "config-manager";
|
||||||
* from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
* from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
||||||
*/
|
*/
|
||||||
export const str2ab = (str: string) => {
|
export const str2ab = (str: string) => {
|
||||||
const buf = new ArrayBuffer(str.length);
|
const buf = new ArrayBuffer(str.length);
|
||||||
const bufView = new Uint8Array(buf);
|
const bufView = new Uint8Array(buf);
|
||||||
for (let i = 0, strLen = str.length; i < strLen; i++) {
|
for (let i = 0, strLen = str.length; i < strLen; i++) {
|
||||||
bufView[i] = str.charCodeAt(i);
|
bufView[i] = str.charCodeAt(i);
|
||||||
}
|
}
|
||||||
return buf;
|
return buf;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const federateStatusTo = async (
|
export const federateStatusTo = async (
|
||||||
status: StatusWithRelations,
|
status: StatusWithRelations,
|
||||||
sender: User,
|
sender: User,
|
||||||
user: User
|
user: User,
|
||||||
) => {
|
) => {
|
||||||
const privateKey = await crypto.subtle.importKey(
|
const privateKey = await crypto.subtle.importKey(
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
str2ab(atob(user.privateKey ?? "")),
|
str2ab(atob(user.privateKey ?? "")),
|
||||||
"Ed25519",
|
"Ed25519",
|
||||||
false,
|
false,
|
||||||
["sign"]
|
["sign"],
|
||||||
);
|
);
|
||||||
|
|
||||||
const digest = await crypto.subtle.digest(
|
const digest = await crypto.subtle.digest(
|
||||||
"SHA-256",
|
"SHA-256",
|
||||||
new TextEncoder().encode("request_body")
|
new TextEncoder().encode("request_body"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
const userInbox = new URL(user.endpoints.inbox);
|
const userInbox = new URL(user.endpoints.inbox);
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
|
||||||
const signature = await crypto.subtle.sign(
|
const signature = await crypto.subtle.sign(
|
||||||
"Ed25519",
|
"Ed25519",
|
||||||
privateKey,
|
privateKey,
|
||||||
new TextEncoder().encode(
|
new TextEncoder().encode(
|
||||||
`(request-target): post ${userInbox.pathname}\n` +
|
`(request-target): post ${userInbox.pathname}\n` +
|
||||||
`host: ${userInbox.host}\n` +
|
`host: ${userInbox.host}\n` +
|
||||||
`date: ${date.toUTCString()}\n` +
|
`date: ${date.toUTCString()}\n` +
|
||||||
`digest: SHA-256=${btoa(
|
`digest: SHA-256=${btoa(
|
||||||
String.fromCharCode(...new Uint8Array(digest))
|
String.fromCharCode(...new Uint8Array(digest)),
|
||||||
)}\n`
|
)}\n`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const signatureBase64 = btoa(
|
const signatureBase64 = btoa(
|
||||||
String.fromCharCode(...new Uint8Array(signature))
|
String.fromCharCode(...new Uint8Array(signature)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return fetch(userInbox, {
|
return fetch(userInbox, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Date: date.toUTCString(),
|
Date: date.toUTCString(),
|
||||||
Origin: config.http.base_url,
|
Origin: config.http.base_url,
|
||||||
Signature: `keyId="${sender.uri}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
Signature: `keyId="${sender.uri}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(statusToLysand(status)),
|
body: JSON.stringify(statusToLysand(status)),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addStatusFederationJob = async (statusId: string) => {
|
export const addStatusFederationJob = async (statusId: string) => {
|
||||||
/* await federationQueue.add("federation", {
|
/* await federationQueue.add("federation", {
|
||||||
id: statusId,
|
id: statusId,
|
||||||
}); */
|
}); */
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Relationship, User } from "@prisma/client";
|
import type { Relationship, User } from "@prisma/client";
|
||||||
import type { APIRelationship } from "~types/entities/relationship";
|
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import type { APIRelationship } from "~types/entities/relationship";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores Mastodon API relationships
|
* Stores Mastodon API relationships
|
||||||
|
|
@ -13,55 +13,55 @@ import { client } from "~database/datasource";
|
||||||
* @returns The newly created relationship.
|
* @returns The newly created relationship.
|
||||||
*/
|
*/
|
||||||
export const createNewRelationship = async (
|
export const createNewRelationship = async (
|
||||||
owner: User,
|
owner: User,
|
||||||
other: User
|
other: User,
|
||||||
): Promise<Relationship> => {
|
): Promise<Relationship> => {
|
||||||
return await client.relationship.create({
|
return await client.relationship.create({
|
||||||
data: {
|
data: {
|
||||||
ownerId: owner.id,
|
ownerId: owner.id,
|
||||||
subjectId: other.id,
|
subjectId: other.id,
|
||||||
languages: [],
|
languages: [],
|
||||||
following: false,
|
following: false,
|
||||||
showingReblogs: false,
|
showingReblogs: false,
|
||||||
notifying: false,
|
notifying: false,
|
||||||
followedBy: false,
|
followedBy: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
blockedBy: false,
|
blockedBy: false,
|
||||||
muting: false,
|
muting: false,
|
||||||
mutingNotifications: false,
|
mutingNotifications: false,
|
||||||
requested: false,
|
requested: false,
|
||||||
domainBlocking: false,
|
domainBlocking: false,
|
||||||
endorsed: false,
|
endorsed: false,
|
||||||
note: "",
|
note: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkForBidirectionalRelationships = async (
|
export const checkForBidirectionalRelationships = async (
|
||||||
user1: User,
|
user1: User,
|
||||||
user2: User,
|
user2: User,
|
||||||
createIfNotExists = true
|
createIfNotExists = true,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const relationship1 = await client.relationship.findFirst({
|
const relationship1 = await client.relationship.findFirst({
|
||||||
where: {
|
where: {
|
||||||
ownerId: user1.id,
|
ownerId: user1.id,
|
||||||
subjectId: user2.id,
|
subjectId: user2.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const relationship2 = await client.relationship.findFirst({
|
const relationship2 = await client.relationship.findFirst({
|
||||||
where: {
|
where: {
|
||||||
ownerId: user2.id,
|
ownerId: user2.id,
|
||||||
subjectId: user1.id,
|
subjectId: user1.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!relationship1 && !relationship2 && createIfNotExists) {
|
if (!relationship1 && !relationship2 && createIfNotExists) {
|
||||||
await createNewRelationship(user1, user2);
|
await createNewRelationship(user1, user2);
|
||||||
await createNewRelationship(user2, user1);
|
await createNewRelationship(user2, user1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!relationship1 && !!relationship2;
|
return !!relationship1 && !!relationship2;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,20 +69,20 @@ export const checkForBidirectionalRelationships = async (
|
||||||
* @returns The API-friendly relationship.
|
* @returns The API-friendly relationship.
|
||||||
*/
|
*/
|
||||||
export const relationshipToAPI = (rel: Relationship): APIRelationship => {
|
export const relationshipToAPI = (rel: Relationship): APIRelationship => {
|
||||||
return {
|
return {
|
||||||
blocked_by: rel.blockedBy,
|
blocked_by: rel.blockedBy,
|
||||||
blocking: rel.blocking,
|
blocking: rel.blocking,
|
||||||
domain_blocking: rel.domainBlocking,
|
domain_blocking: rel.domainBlocking,
|
||||||
endorsed: rel.endorsed,
|
endorsed: rel.endorsed,
|
||||||
followed_by: rel.followedBy,
|
followed_by: rel.followedBy,
|
||||||
following: rel.following,
|
following: rel.following,
|
||||||
id: rel.subjectId,
|
id: rel.subjectId,
|
||||||
muting: rel.muting,
|
muting: rel.muting,
|
||||||
muting_notifications: rel.mutingNotifications,
|
muting_notifications: rel.mutingNotifications,
|
||||||
notifying: rel.notifying,
|
notifying: rel.notifying,
|
||||||
requested: rel.requested,
|
requested: rel.requested,
|
||||||
showing_reblogs: rel.showingReblogs,
|
showing_reblogs: rel.showingReblogs,
|
||||||
languages: rel.languages,
|
languages: rel.languages,
|
||||||
note: rel.note,
|
note: rel.note,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
|
import { getBestContentType } from "@content_types";
|
||||||
|
import { addStausToMeilisearch } from "@meilisearch";
|
||||||
|
import {
|
||||||
|
type Application,
|
||||||
|
type Emoji,
|
||||||
|
Prisma,
|
||||||
|
type Relationship,
|
||||||
|
type Status,
|
||||||
|
type User,
|
||||||
|
} from "@prisma/client";
|
||||||
|
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 { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import type { UserWithRelations } from "./User";
|
import type { UserWithRelations } from "./User";
|
||||||
import { fetchRemoteUser, parseMentionsUris, userToAPI } 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 {
|
|
||||||
Prisma,
|
|
||||||
type Application,
|
|
||||||
type Emoji,
|
|
||||||
type Relationship,
|
|
||||||
type Status,
|
|
||||||
type User,
|
|
||||||
} from "@prisma/client";
|
|
||||||
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
|
|
||||||
import type { APIStatus } from "~types/entities/status";
|
|
||||||
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 { statusAndUserRelations, userRelations } from "./relations";
|
import { statusAndUserRelations, userRelations } from "./relations";
|
||||||
|
|
||||||
const statusRelations = Prisma.validator<Prisma.StatusDefaultArgs>()({
|
const statusRelations = Prisma.validator<Prisma.StatusDefaultArgs>()({
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type StatusWithRelations = Prisma.StatusGetPayload<
|
export type StatusWithRelations = Prisma.StatusGetPayload<
|
||||||
typeof statusRelations
|
typeof statusRelations
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -44,76 +44,75 @@ export type StatusWithRelations = Prisma.StatusGetPayload<
|
||||||
* @returns Whether this status is viewable by the user.
|
* @returns Whether this status is viewable by the user.
|
||||||
*/
|
*/
|
||||||
export const isViewableByUser = (status: Status, user: User | null) => {
|
export const isViewableByUser = (status: Status, user: User | null) => {
|
||||||
if (status.authorId === user?.id) return true;
|
if (status.authorId === user?.id) return true;
|
||||||
if (status.visibility === "public") return true;
|
if (status.visibility === "public") return true;
|
||||||
else if (status.visibility === "unlisted") return true;
|
if (status.visibility === "unlisted") return true;
|
||||||
else if (status.visibility === "private") {
|
if (status.visibility === "private") {
|
||||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||||
return !!(user?.relationships as Relationship[]).find(
|
return !!(user?.relationships as Relationship[]).find(
|
||||||
rel => rel.id === status.authorId
|
(rel) => rel.id === status.authorId,
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||||
return user && (status.mentions as User[]).includes(user);
|
return user && (status.mentions as User[]).includes(user);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
|
export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
|
||||||
// Check if already in database
|
// Check if already in database
|
||||||
|
|
||||||
const existingStatus: StatusWithRelations | null =
|
const existingStatus: StatusWithRelations | null =
|
||||||
await client.status.findFirst({
|
await client.status.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uri: uri,
|
uri: uri,
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingStatus) return existingStatus;
|
if (existingStatus) return existingStatus;
|
||||||
|
|
||||||
const status = await fetch(uri);
|
const status = await fetch(uri);
|
||||||
|
|
||||||
if (status.status === 404) return null;
|
if (status.status === 404) return null;
|
||||||
|
|
||||||
const body = (await status.json()) as LysandPublication;
|
const body = (await status.json()) as LysandPublication;
|
||||||
|
|
||||||
const content = getBestContentType(body.contents);
|
const content = getBestContentType(body.contents);
|
||||||
|
|
||||||
const emojis = await parseEmojis(content?.content || "");
|
const emojis = await parseEmojis(content?.content || "");
|
||||||
|
|
||||||
const author = await fetchRemoteUser(body.author);
|
const author = await fetchRemoteUser(body.author);
|
||||||
|
|
||||||
let replyStatus: Status | null = null;
|
let replyStatus: Status | null = null;
|
||||||
let quotingStatus: Status | null = null;
|
let quotingStatus: Status | null = null;
|
||||||
|
|
||||||
if (body.replies_to.length > 0) {
|
if (body.replies_to.length > 0) {
|
||||||
replyStatus = await fetchFromRemote(body.replies_to[0]);
|
replyStatus = await fetchFromRemote(body.replies_to[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.quotes.length > 0) {
|
if (body.quotes.length > 0) {
|
||||||
quotingStatus = await fetchFromRemote(body.quotes[0]);
|
quotingStatus = await fetchFromRemote(body.quotes[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await createNewStatus({
|
return await createNewStatus({
|
||||||
account: author,
|
account: author,
|
||||||
content: content?.content || "",
|
content: content?.content || "",
|
||||||
content_type: content?.content_type,
|
content_type: content?.content_type,
|
||||||
application: null,
|
application: null,
|
||||||
// TODO: Add visibility
|
// TODO: Add visibility
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
spoiler_text: body.subject || "",
|
spoiler_text: body.subject || "",
|
||||||
uri: body.uri,
|
uri: body.uri,
|
||||||
sensitive: body.is_sensitive,
|
sensitive: body.is_sensitive,
|
||||||
emojis: emojis,
|
emojis: emojis,
|
||||||
mentions: await parseMentionsUris(body.mentions),
|
mentions: await parseMentionsUris(body.mentions),
|
||||||
reply: replyStatus
|
reply: replyStatus
|
||||||
? {
|
? {
|
||||||
status: replyStatus,
|
status: replyStatus,
|
||||||
user: (replyStatus as any).author,
|
user: (replyStatus as StatusWithRelations).author,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
quote: quotingStatus || undefined,
|
quote: quotingStatus || undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -121,34 +120,34 @@ export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
||||||
export const getAncestors = async (
|
export const getAncestors = async (
|
||||||
status: StatusWithRelations,
|
status: StatusWithRelations,
|
||||||
fetcher: UserWithRelations | null
|
fetcher: UserWithRelations | null,
|
||||||
) => {
|
) => {
|
||||||
const ancestors: StatusWithRelations[] = [];
|
const ancestors: StatusWithRelations[] = [];
|
||||||
|
|
||||||
let currentStatus = status;
|
let currentStatus = status;
|
||||||
|
|
||||||
while (currentStatus.inReplyToPostId) {
|
while (currentStatus.inReplyToPostId) {
|
||||||
const parent = await client.status.findFirst({
|
const parent = await client.status.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: currentStatus.inReplyToPostId,
|
id: currentStatus.inReplyToPostId,
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!parent) break;
|
if (!parent) break;
|
||||||
|
|
||||||
ancestors.push(parent);
|
ancestors.push(parent);
|
||||||
|
|
||||||
currentStatus = parent;
|
currentStatus = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter for posts that are viewable by the user
|
// Filter for posts that are viewable by the user
|
||||||
|
|
||||||
const viewableAncestors = ancestors.filter(ancestor =>
|
const viewableAncestors = ancestors.filter((ancestor) =>
|
||||||
isViewableByUser(ancestor, fetcher)
|
isViewableByUser(ancestor, fetcher),
|
||||||
);
|
);
|
||||||
return viewableAncestors;
|
return viewableAncestors;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -157,42 +156,42 @@ export const getAncestors = async (
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
||||||
export const getDescendants = async (
|
export const getDescendants = async (
|
||||||
status: StatusWithRelations,
|
status: StatusWithRelations,
|
||||||
fetcher: UserWithRelations | null,
|
fetcher: UserWithRelations | null,
|
||||||
depth = 0
|
depth = 0,
|
||||||
) => {
|
) => {
|
||||||
const descendants: StatusWithRelations[] = [];
|
const descendants: StatusWithRelations[] = [];
|
||||||
|
|
||||||
const currentStatus = status;
|
const currentStatus = status;
|
||||||
|
|
||||||
// Fetch all children of children of children recursively calling getDescendants
|
// Fetch all children of children of children recursively calling getDescendants
|
||||||
|
|
||||||
const children = await client.status.findMany({
|
const children = await client.status.findMany({
|
||||||
where: {
|
where: {
|
||||||
inReplyToPostId: currentStatus.id,
|
inReplyToPostId: currentStatus.id,
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
descendants.push(child);
|
descendants.push(child);
|
||||||
|
|
||||||
if (depth < 20) {
|
if (depth < 20) {
|
||||||
const childDescendants = await getDescendants(
|
const childDescendants = await getDescendants(
|
||||||
child,
|
child,
|
||||||
fetcher,
|
fetcher,
|
||||||
depth + 1
|
depth + 1,
|
||||||
);
|
);
|
||||||
descendants.push(...childDescendants);
|
descendants.push(...childDescendants);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter for posts that are viewable by the user
|
// Filter for posts that are viewable by the user
|
||||||
|
|
||||||
const viewableDescendants = descendants.filter(descendant =>
|
const viewableDescendants = descendants.filter((descendant) =>
|
||||||
isViewableByUser(descendant, fetcher)
|
isViewableByUser(descendant, fetcher),
|
||||||
);
|
);
|
||||||
return viewableDescendants;
|
return viewableDescendants;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -201,250 +200,250 @@ export const getDescendants = async (
|
||||||
* @returns A promise that resolves with the new status.
|
* @returns A promise that resolves with the new status.
|
||||||
*/
|
*/
|
||||||
export const createNewStatus = async (data: {
|
export const createNewStatus = async (data: {
|
||||||
account: User;
|
account: User;
|
||||||
application: Application | null;
|
application: Application | null;
|
||||||
content: string;
|
content: string;
|
||||||
visibility: APIStatus["visibility"];
|
visibility: APIStatus["visibility"];
|
||||||
sensitive: boolean;
|
sensitive: boolean;
|
||||||
spoiler_text: string;
|
spoiler_text: string;
|
||||||
emojis?: Emoji[];
|
emojis?: Emoji[];
|
||||||
content_type?: string;
|
content_type?: string;
|
||||||
uri?: string;
|
uri?: string;
|
||||||
mentions?: User[];
|
mentions?: User[];
|
||||||
media_attachments?: string[];
|
media_attachments?: string[];
|
||||||
reply?: {
|
reply?: {
|
||||||
status: Status;
|
status: Status;
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
quote?: Status;
|
quote?: Status;
|
||||||
}) => {
|
}) => {
|
||||||
// Get people mentioned in the content (match @username or @username@domain.com mentions)
|
// Get people mentioned in the content (match @username or @username@domain.com mentions)
|
||||||
const mentionedPeople =
|
const mentionedPeople =
|
||||||
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
||||||
|
|
||||||
let mentions = data.mentions || [];
|
let mentions = data.mentions || [];
|
||||||
|
|
||||||
// Parse emojis
|
// Parse emojis
|
||||||
const emojis = await parseEmojis(data.content);
|
const emojis = await parseEmojis(data.content);
|
||||||
|
|
||||||
data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis;
|
data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis;
|
||||||
|
|
||||||
// Get list of mentioned users
|
// Get list of mentioned users
|
||||||
if (mentions.length === 0) {
|
if (mentions.length === 0) {
|
||||||
mentions = await client.user.findMany({
|
mentions = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: mentionedPeople.map(person => ({
|
OR: mentionedPeople.map((person) => ({
|
||||||
username: person.split("@")[1],
|
username: person.split("@")[1],
|
||||||
instance: {
|
instance: {
|
||||||
base_url: person.split("@")[2],
|
base_url: person.split("@")[2],
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let formattedContent;
|
let formattedContent = "";
|
||||||
|
|
||||||
// Get HTML version of content
|
// Get HTML version of content
|
||||||
if (data.content_type === "text/markdown") {
|
if (data.content_type === "text/markdown") {
|
||||||
formattedContent = linkifyHtml(
|
formattedContent = linkifyHtml(
|
||||||
await sanitizeHtml(await parse(data.content))
|
await sanitizeHtml(await parse(data.content)),
|
||||||
);
|
);
|
||||||
} else if (data.content_type === "text/x.misskeymarkdown") {
|
} else if (data.content_type === "text/x.misskeymarkdown") {
|
||||||
// Parse as MFM
|
// Parse as MFM
|
||||||
} else {
|
} else {
|
||||||
// Parse as plaintext
|
// Parse as plaintext
|
||||||
formattedContent = linkifyStr(data.content);
|
formattedContent = linkifyStr(data.content);
|
||||||
|
|
||||||
// Split by newline and add <p> tags
|
// Split by newline and add <p> tags
|
||||||
formattedContent = formattedContent
|
formattedContent = formattedContent
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(line => `<p>${line}</p>`)
|
.map((line) => `<p>${line}</p>`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = await client.status.create({
|
let status = await client.status.create({
|
||||||
data: {
|
data: {
|
||||||
authorId: data.account.id,
|
authorId: data.account.id,
|
||||||
applicationId: data.application?.id,
|
applicationId: data.application?.id,
|
||||||
content: formattedContent,
|
content: formattedContent,
|
||||||
contentSource: data.content,
|
contentSource: data.content,
|
||||||
contentType: data.content_type,
|
contentType: data.content_type,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
sensitive: data.sensitive,
|
sensitive: data.sensitive,
|
||||||
spoilerText: data.spoiler_text,
|
spoilerText: data.spoiler_text,
|
||||||
emojis: {
|
emojis: {
|
||||||
connect: data.emojis.map(emoji => {
|
connect: data.emojis.map((emoji) => {
|
||||||
return {
|
return {
|
||||||
id: emoji.id,
|
id: emoji.id,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
attachments: data.media_attachments
|
attachments: data.media_attachments
|
||||||
? {
|
? {
|
||||||
connect: data.media_attachments.map(attachment => {
|
connect: data.media_attachments.map((attachment) => {
|
||||||
return {
|
return {
|
||||||
id: attachment,
|
id: attachment,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
inReplyToPostId: data.reply?.status.id,
|
inReplyToPostId: data.reply?.status.id,
|
||||||
quotingPostId: data.quote?.id,
|
quotingPostId: data.quote?.id,
|
||||||
instanceId: data.account.instanceId || undefined,
|
instanceId: data.account.instanceId || undefined,
|
||||||
isReblog: false,
|
isReblog: false,
|
||||||
uri:
|
uri:
|
||||||
data.uri ||
|
data.uri ||
|
||||||
`${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`,
|
`${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`,
|
||||||
mentions: {
|
mentions: {
|
||||||
connect: mentions.map(mention => {
|
connect: mentions.map((mention) => {
|
||||||
return {
|
return {
|
||||||
id: mention.id,
|
id: mention.id,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update URI
|
// Update URI
|
||||||
status = await client.status.update({
|
status = await client.status.update({
|
||||||
where: {
|
where: {
|
||||||
id: status.id,
|
id: status.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
uri: data.uri || `${config.http.base_url}/statuses/${status.id}`,
|
uri: data.uri || `${config.http.base_url}/statuses/${status.id}`,
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create notification
|
// Create notification
|
||||||
if (status.inReplyToPost) {
|
if (status.inReplyToPost) {
|
||||||
await client.notification.create({
|
await client.notification.create({
|
||||||
data: {
|
data: {
|
||||||
notifiedId: status.inReplyToPost.authorId,
|
notifiedId: status.inReplyToPost.authorId,
|
||||||
accountId: status.authorId,
|
accountId: status.authorId,
|
||||||
type: "mention",
|
type: "mention",
|
||||||
statusId: status.id,
|
statusId: status.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to search index
|
// Add to search index
|
||||||
await addStausToMeilisearch(status);
|
await addStausToMeilisearch(status);
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editStatus = async (
|
export const editStatus = async (
|
||||||
status: StatusWithRelations,
|
status: StatusWithRelations,
|
||||||
data: {
|
data: {
|
||||||
content: string;
|
content: string;
|
||||||
visibility?: APIStatus["visibility"];
|
visibility?: APIStatus["visibility"];
|
||||||
sensitive: boolean;
|
sensitive: boolean;
|
||||||
spoiler_text: string;
|
spoiler_text: string;
|
||||||
emojis?: Emoji[];
|
emojis?: Emoji[];
|
||||||
content_type?: string;
|
content_type?: string;
|
||||||
uri?: string;
|
uri?: string;
|
||||||
mentions?: User[];
|
mentions?: User[];
|
||||||
media_attachments?: string[];
|
media_attachments?: string[];
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
// Get people mentioned in the content (match @username or @username@domain.com mentions
|
// Get people mentioned in the content (match @username or @username@domain.com mentions
|
||||||
const mentionedPeople =
|
const mentionedPeople =
|
||||||
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
||||||
|
|
||||||
let mentions = data.mentions || [];
|
let mentions = data.mentions || [];
|
||||||
|
|
||||||
// Parse emojis
|
// Parse emojis
|
||||||
const emojis = await parseEmojis(data.content);
|
const emojis = await parseEmojis(data.content);
|
||||||
|
|
||||||
data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis;
|
data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis;
|
||||||
|
|
||||||
// Get list of mentioned users
|
// Get list of mentioned users
|
||||||
if (mentions.length === 0) {
|
if (mentions.length === 0) {
|
||||||
mentions = await client.user.findMany({
|
mentions = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: mentionedPeople.map(person => ({
|
OR: mentionedPeople.map((person) => ({
|
||||||
username: person.split("@")[1],
|
username: person.split("@")[1],
|
||||||
instance: {
|
instance: {
|
||||||
base_url: person.split("@")[2],
|
base_url: person.split("@")[2],
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let formattedContent;
|
let formattedContent = "";
|
||||||
|
|
||||||
// Get HTML version of content
|
// Get HTML version of content
|
||||||
if (data.content_type === "text/markdown") {
|
if (data.content_type === "text/markdown") {
|
||||||
formattedContent = linkifyHtml(
|
formattedContent = linkifyHtml(
|
||||||
await sanitizeHtml(await parse(data.content))
|
await sanitizeHtml(await parse(data.content)),
|
||||||
);
|
);
|
||||||
} else if (data.content_type === "text/x.misskeymarkdown") {
|
} else if (data.content_type === "text/x.misskeymarkdown") {
|
||||||
// Parse as MFM
|
// Parse as MFM
|
||||||
} else {
|
} else {
|
||||||
// Parse as plaintext
|
// Parse as plaintext
|
||||||
formattedContent = linkifyStr(data.content);
|
formattedContent = linkifyStr(data.content);
|
||||||
|
|
||||||
// Split by newline and add <p> tags
|
// Split by newline and add <p> tags
|
||||||
formattedContent = formattedContent
|
formattedContent = formattedContent
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(line => `<p>${line}</p>`)
|
.map((line) => `<p>${line}</p>`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const newStatus = await client.status.update({
|
const newStatus = await client.status.update({
|
||||||
where: {
|
where: {
|
||||||
id: status.id,
|
id: status.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
content: formattedContent,
|
content: formattedContent,
|
||||||
contentSource: data.content,
|
contentSource: data.content,
|
||||||
contentType: data.content_type,
|
contentType: data.content_type,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
sensitive: data.sensitive,
|
sensitive: data.sensitive,
|
||||||
spoilerText: data.spoiler_text,
|
spoilerText: data.spoiler_text,
|
||||||
emojis: {
|
emojis: {
|
||||||
connect: data.emojis.map(emoji => {
|
connect: data.emojis.map((emoji) => {
|
||||||
return {
|
return {
|
||||||
id: emoji.id,
|
id: emoji.id,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
attachments: data.media_attachments
|
attachments: data.media_attachments
|
||||||
? {
|
? {
|
||||||
connect: data.media_attachments.map(attachment => {
|
connect: data.media_attachments.map((attachment) => {
|
||||||
return {
|
return {
|
||||||
id: attachment,
|
id: attachment,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
mentions: {
|
mentions: {
|
||||||
connect: mentions.map(mention => {
|
connect: mentions.map((mention) => {
|
||||||
return {
|
return {
|
||||||
id: mention.id,
|
id: mention.id,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
return newStatus;
|
return newStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFavouritedBy = async (status: Status, user: User) => {
|
export const isFavouritedBy = async (status: Status, user: User) => {
|
||||||
return !!(await client.like.findFirst({
|
return !!(await client.like.findFirst({
|
||||||
where: {
|
where: {
|
||||||
likerId: user.id,
|
likerId: user.id,
|
||||||
likedId: status.id,
|
likedId: status.id,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -452,67 +451,67 @@ export const isFavouritedBy = async (status: Status, user: User) => {
|
||||||
* @returns A promise that resolves with the API status.
|
* @returns A promise that resolves with the API status.
|
||||||
*/
|
*/
|
||||||
export const statusToAPI = async (
|
export const statusToAPI = async (
|
||||||
status: StatusWithRelations,
|
status: StatusWithRelations,
|
||||||
user?: UserWithRelations
|
user?: UserWithRelations,
|
||||||
): Promise<APIStatus> => {
|
): Promise<APIStatus> => {
|
||||||
return {
|
return {
|
||||||
id: status.id,
|
id: status.id,
|
||||||
in_reply_to_id: status.inReplyToPostId || null,
|
in_reply_to_id: status.inReplyToPostId || null,
|
||||||
in_reply_to_account_id: status.inReplyToPost?.authorId || null,
|
in_reply_to_account_id: status.inReplyToPost?.authorId || null,
|
||||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||||
account: userToAPI(status.author),
|
account: userToAPI(status.author),
|
||||||
created_at: new Date(status.createdAt).toISOString(),
|
created_at: new Date(status.createdAt).toISOString(),
|
||||||
application: status.application
|
application: status.application
|
||||||
? applicationToAPI(status.application)
|
? applicationToAPI(status.application)
|
||||||
: null,
|
: null,
|
||||||
card: null,
|
card: null,
|
||||||
content: status.content,
|
content: status.content,
|
||||||
emojis: status.emojis.map(emoji => emojiToAPI(emoji)),
|
emojis: status.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
favourited: !!(status.likes ?? []).find(
|
favourited: !!(status.likes ?? []).find(
|
||||||
like => like.likerId === user?.id
|
(like) => like.likerId === user?.id,
|
||||||
),
|
),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
favourites_count: (status.likes ?? []).length,
|
favourites_count: (status.likes ?? []).length,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
media_attachments: (status.attachments ?? []).map(
|
media_attachments: (status.attachments ?? []).map(
|
||||||
a => attachmentToAPI(a) as APIAttachment
|
(a) => attachmentToAPI(a) as APIAttachment,
|
||||||
),
|
),
|
||||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||||
mentions: status.mentions.map(mention => userToAPI(mention)),
|
mentions: status.mentions.map((mention) => userToAPI(mention)),
|
||||||
language: null,
|
language: null,
|
||||||
muted: user
|
muted: user
|
||||||
? user.relationships.find(r => r.subjectId == status.authorId)
|
? user.relationships.find((r) => r.subjectId === status.authorId)
|
||||||
?.muting || false
|
?.muting || false
|
||||||
: false,
|
: false,
|
||||||
pinned: status.pinnedBy.find(u => u.id === user?.id) ? true : false,
|
pinned: status.pinnedBy.find((u) => u.id === user?.id) ? true : false,
|
||||||
// TODO: Add pols
|
// TODO: Add pols
|
||||||
poll: null,
|
poll: null,
|
||||||
reblog: status.reblog
|
reblog: status.reblog
|
||||||
? await statusToAPI(status.reblog as unknown as StatusWithRelations)
|
? await statusToAPI(status.reblog as unknown as StatusWithRelations)
|
||||||
: null,
|
: null,
|
||||||
reblogged: !!(await client.status.findFirst({
|
reblogged: !!(await client.status.findFirst({
|
||||||
where: {
|
where: {
|
||||||
authorId: user?.id,
|
authorId: user?.id,
|
||||||
reblogId: status.id,
|
reblogId: status.id,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
reblogs_count: status._count.reblogs,
|
reblogs_count: status._count.reblogs,
|
||||||
replies_count: status._count.replies,
|
replies_count: status._count.replies,
|
||||||
sensitive: status.sensitive,
|
sensitive: status.sensitive,
|
||||||
spoiler_text: status.spoilerText,
|
spoiler_text: status.spoilerText,
|
||||||
tags: [],
|
tags: [],
|
||||||
uri: `${config.http.base_url}/statuses/${status.id}`,
|
uri: `${config.http.base_url}/statuses/${status.id}`,
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
url: `${config.http.base_url}/statuses/${status.id}`,
|
url: `${config.http.base_url}/statuses/${status.id}`,
|
||||||
bookmarked: false,
|
bookmarked: false,
|
||||||
quote: status.quotingPost
|
quote: status.quotingPost
|
||||||
? await statusToAPI(
|
? await statusToAPI(
|
||||||
status.quotingPost as unknown as StatusWithRelations
|
status.quotingPost as unknown as StatusWithRelations,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
quote_id: status.quotingPost?.id || undefined,
|
quote_id: status.quotingPost?.id || undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* export const statusToActivityPub = async (
|
/* export const statusToActivityPub = async (
|
||||||
|
|
@ -563,35 +562,35 @@ export const statusToAPI = async (
|
||||||
}; */
|
}; */
|
||||||
|
|
||||||
export const statusToLysand = (status: StatusWithRelations): Note => {
|
export const statusToLysand = (status: StatusWithRelations): Note => {
|
||||||
return {
|
return {
|
||||||
type: "Note",
|
type: "Note",
|
||||||
created_at: new Date(status.createdAt).toISOString(),
|
created_at: new Date(status.createdAt).toISOString(),
|
||||||
id: status.id,
|
id: status.id,
|
||||||
author: status.authorId,
|
author: status.authorId,
|
||||||
uri: `${config.http.base_url}/users/${status.authorId}/statuses/${status.id}`,
|
uri: `${config.http.base_url}/users/${status.authorId}/statuses/${status.id}`,
|
||||||
contents: [
|
contents: [
|
||||||
{
|
{
|
||||||
content: status.content,
|
content: status.content,
|
||||||
content_type: "text/html",
|
content_type: "text/html",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Content converted to plaintext
|
// Content converted to plaintext
|
||||||
content: htmlToText(status.content),
|
content: htmlToText(status.content),
|
||||||
content_type: "text/plain",
|
content_type: "text/plain",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// TODO: Add attachments
|
// TODO: Add attachments
|
||||||
attachments: [],
|
attachments: [],
|
||||||
is_sensitive: status.sensitive,
|
is_sensitive: status.sensitive,
|
||||||
mentions: status.mentions.map(mention => mention.uri),
|
mentions: status.mentions.map((mention) => mention.uri),
|
||||||
quotes: status.quotingPost ? [status.quotingPost.uri] : [],
|
quotes: status.quotingPost ? [status.quotingPost.uri] : [],
|
||||||
replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [],
|
replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [],
|
||||||
subject: status.spoilerText,
|
subject: status.spoilerText,
|
||||||
extensions: {
|
extensions: {
|
||||||
"org.lysand:custom_emojis": {
|
"org.lysand:custom_emojis": {
|
||||||
emojis: status.emojis.map(emoji => emojiToLysand(emoji)),
|
emojis: status.emojis.map((emoji) => emojiToLysand(emoji)),
|
||||||
},
|
},
|
||||||
// TODO: Add polls and reactions
|
// TODO: Add polls and reactions
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
* The type of token.
|
* The type of token.
|
||||||
*/
|
*/
|
||||||
export enum TokenType {
|
export enum TokenType {
|
||||||
BEARER = "Bearer",
|
BEARER = "Bearer",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import type { APIAccount } from "~types/entities/account";
|
import { addUserToMeilisearch } from "@meilisearch";
|
||||||
import type { LysandUser } from "~types/lysand/Object";
|
|
||||||
import { htmlToText } from "html-to-text";
|
|
||||||
import type { User } from "@prisma/client";
|
import type { User } from "@prisma/client";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { type Config, config } from "config-manager";
|
||||||
|
import { htmlToText } from "html-to-text";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import { MediaBackendType } from "~packages/media-manager";
|
||||||
|
import type { APIAccount } from "~types/entities/account";
|
||||||
|
import type { APISource } from "~types/entities/source";
|
||||||
|
import type { LysandUser } from "~types/lysand/Object";
|
||||||
import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji";
|
import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji";
|
||||||
import { addInstanceIfNotExists } from "./Instance";
|
import { addInstanceIfNotExists } from "./Instance";
|
||||||
import type { APISource } from "~types/entities/source";
|
|
||||||
import { addUserToMeilisearch } from "@meilisearch";
|
|
||||||
import { config, type Config } from "config-manager";
|
|
||||||
import { userRelations } from "./relations";
|
import { userRelations } from "./relations";
|
||||||
import { MediaBackendType } from "~packages/media-manager";
|
|
||||||
|
|
||||||
export interface AuthData {
|
export interface AuthData {
|
||||||
user: UserWithRelations | null;
|
user: UserWithRelations | null;
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,7 +23,7 @@ export interface AuthData {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const userRelations2 = Prisma.validator<Prisma.UserDefaultArgs>()({
|
const userRelations2 = Prisma.validator<Prisma.UserDefaultArgs>()({
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UserWithRelations = Prisma.UserGetPayload<typeof userRelations2>;
|
export type UserWithRelations = Prisma.UserGetPayload<typeof userRelations2>;
|
||||||
|
|
@ -34,14 +34,15 @@ export type UserWithRelations = Prisma.UserGetPayload<typeof userRelations2>;
|
||||||
* @returns The raw URL for the user's avatar
|
* @returns The raw URL for the user's avatar
|
||||||
*/
|
*/
|
||||||
export const getAvatarUrl = (user: User, config: Config) => {
|
export const getAvatarUrl = (user: User, config: Config) => {
|
||||||
if (!user.avatar) return config.defaults.avatar;
|
if (!user.avatar) return config.defaults.avatar;
|
||||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||||
return `${config.http.base_url}/media/${user.avatar}`;
|
return `${config.http.base_url}/media/${user.avatar}`;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
} else if (config.media.backend === MediaBackendType.S3) {
|
}
|
||||||
return `${config.s3.public_url}/${user.avatar}`;
|
if (config.media.backend === MediaBackendType.S3) {
|
||||||
}
|
return `${config.s3.public_url}/${user.avatar}`;
|
||||||
return "";
|
}
|
||||||
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -50,128 +51,129 @@ export const getAvatarUrl = (user: User, config: Config) => {
|
||||||
* @returns The raw URL for the user's header
|
* @returns The raw URL for the user's header
|
||||||
*/
|
*/
|
||||||
export const getHeaderUrl = (user: User, config: Config) => {
|
export const getHeaderUrl = (user: User, config: Config) => {
|
||||||
if (!user.header) return config.defaults.header;
|
if (!user.header) return config.defaults.header;
|
||||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||||
return `${config.http.base_url}/media/${user.header}`;
|
return `${config.http.base_url}/media/${user.header}`;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
} else if (config.media.backend === MediaBackendType.S3) {
|
}
|
||||||
return `${config.s3.public_url}/${user.header}`;
|
if (config.media.backend === MediaBackendType.S3) {
|
||||||
}
|
return `${config.s3.public_url}/${user.header}`;
|
||||||
return "";
|
}
|
||||||
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFromRequest = async (req: Request): Promise<AuthData> => {
|
export const getFromRequest = async (req: Request): Promise<AuthData> => {
|
||||||
// Check auth token
|
// Check auth token
|
||||||
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
||||||
|
|
||||||
return { user: await retrieveUserFromToken(token), token };
|
return { user: await retrieveUserFromToken(token), token };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRemoteUser = async (uri: string) => {
|
export const fetchRemoteUser = async (uri: string) => {
|
||||||
// Check if user not already in database
|
// Check if user not already in database
|
||||||
const foundUser = await client.user.findUnique({
|
const foundUser = await client.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
uri,
|
uri,
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (foundUser) return foundUser;
|
if (foundUser) return foundUser;
|
||||||
|
|
||||||
const response = await fetch(uri, {
|
const response = await fetch(uri, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = (await response.json()) as Partial<LysandUser>;
|
const data = (await response.json()) as Partial<LysandUser>;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
data.id &&
|
data.id &&
|
||||||
data.username &&
|
data.username &&
|
||||||
data.uri &&
|
data.uri &&
|
||||||
data.created_at &&
|
data.created_at &&
|
||||||
data.disliked &&
|
data.disliked &&
|
||||||
data.featured &&
|
data.featured &&
|
||||||
data.liked &&
|
data.liked &&
|
||||||
data.followers &&
|
data.followers &&
|
||||||
data.following &&
|
data.following &&
|
||||||
data.inbox &&
|
data.inbox &&
|
||||||
data.outbox &&
|
data.outbox &&
|
||||||
data.public_key
|
data.public_key
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new Error("Invalid user data");
|
throw new Error("Invalid user data");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse emojis and add them to database
|
// Parse emojis and add them to database
|
||||||
const userEmojis =
|
const userEmojis =
|
||||||
data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? [];
|
data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? [];
|
||||||
|
|
||||||
const user = await client.user.create({
|
const user = await client.user.create({
|
||||||
data: {
|
data: {
|
||||||
username: data.username,
|
username: data.username,
|
||||||
uri: data.uri,
|
uri: data.uri,
|
||||||
createdAt: new Date(data.created_at),
|
createdAt: new Date(data.created_at),
|
||||||
endpoints: {
|
endpoints: {
|
||||||
disliked: data.disliked,
|
disliked: data.disliked,
|
||||||
featured: data.featured,
|
featured: data.featured,
|
||||||
liked: data.liked,
|
liked: data.liked,
|
||||||
followers: data.followers,
|
followers: data.followers,
|
||||||
following: data.following,
|
following: data.following,
|
||||||
inbox: data.inbox,
|
inbox: data.inbox,
|
||||||
outbox: data.outbox,
|
outbox: data.outbox,
|
||||||
},
|
},
|
||||||
avatar: (data.avatar && data.avatar[0].content) || "",
|
avatar: data.avatar?.[0].content || "",
|
||||||
header: (data.header && data.header[0].content) || "",
|
header: data.header?.[0].content || "",
|
||||||
displayName: data.display_name ?? "",
|
displayName: data.display_name ?? "",
|
||||||
note: data.bio?.[0].content ?? "",
|
note: data.bio?.[0].content ?? "",
|
||||||
publicKey: data.public_key.public_key,
|
publicKey: data.public_key.public_key,
|
||||||
source: {
|
source: {
|
||||||
language: null,
|
language: null,
|
||||||
note: "",
|
note: "",
|
||||||
privacy: "public",
|
privacy: "public",
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
fields: [],
|
fields: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to Meilisearch
|
// Add to Meilisearch
|
||||||
await addUserToMeilisearch(user);
|
await addUserToMeilisearch(user);
|
||||||
|
|
||||||
const emojis = [];
|
const emojis = [];
|
||||||
|
|
||||||
for (const emoji of userEmojis) {
|
for (const emoji of userEmojis) {
|
||||||
emojis.push(await addEmojiIfNotExists(emoji));
|
emojis.push(await addEmojiIfNotExists(emoji));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uriData = new URL(data.uri);
|
const uriData = new URL(data.uri);
|
||||||
|
|
||||||
return await client.user.update({
|
return await client.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
emojis: {
|
emojis: {
|
||||||
connect: emojis.map(emoji => ({
|
connect: emojis.map((emoji) => ({
|
||||||
id: emoji.id,
|
id: emoji.id,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
instanceId: (await addInstanceIfNotExists(uriData.origin)).id,
|
instanceId: (await addInstanceIfNotExists(uriData.origin)).id,
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the list of followers associated with the actor and updates the user's followers
|
* Fetches the list of followers associated with the actor and updates the user's followers
|
||||||
*/
|
*/
|
||||||
export const fetchFollowers = () => {
|
export const fetchFollowers = () => {
|
||||||
//
|
//
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -180,75 +182,75 @@ export const fetchFollowers = () => {
|
||||||
* @returns The newly created user.
|
* @returns The newly created user.
|
||||||
*/
|
*/
|
||||||
export const createNewLocalUser = async (data: {
|
export const createNewLocalUser = async (data: {
|
||||||
username: string;
|
username: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
password: string;
|
password: string;
|
||||||
email: string;
|
email: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
header?: string;
|
header?: string;
|
||||||
admin?: boolean;
|
admin?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const keys = await generateUserKeys();
|
const keys = await generateUserKeys();
|
||||||
|
|
||||||
const user = await client.user.create({
|
const user = await client.user.create({
|
||||||
data: {
|
data: {
|
||||||
username: data.username,
|
username: data.username,
|
||||||
displayName: data.display_name ?? data.username,
|
displayName: data.display_name ?? data.username,
|
||||||
password: await Bun.password.hash(data.password),
|
password: await Bun.password.hash(data.password),
|
||||||
email: data.email,
|
email: data.email,
|
||||||
note: data.bio ?? "",
|
note: data.bio ?? "",
|
||||||
avatar: data.avatar ?? config.defaults.avatar,
|
avatar: data.avatar ?? config.defaults.avatar,
|
||||||
header: data.header ?? config.defaults.avatar,
|
header: data.header ?? config.defaults.avatar,
|
||||||
isAdmin: data.admin ?? false,
|
isAdmin: data.admin ?? false,
|
||||||
uri: "",
|
uri: "",
|
||||||
publicKey: keys.public_key,
|
publicKey: keys.public_key,
|
||||||
privateKey: keys.private_key,
|
privateKey: keys.private_key,
|
||||||
source: {
|
source: {
|
||||||
language: null,
|
language: null,
|
||||||
note: "",
|
note: "",
|
||||||
privacy: "public",
|
privacy: "public",
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
fields: [],
|
fields: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to Meilisearch
|
// Add to Meilisearch
|
||||||
await addUserToMeilisearch(user);
|
await addUserToMeilisearch(user);
|
||||||
|
|
||||||
return await client.user.update({
|
return await client.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
uri: `${config.http.base_url}/users/${user.id}`,
|
uri: `${config.http.base_url}/users/${user.id}`,
|
||||||
endpoints: {
|
endpoints: {
|
||||||
disliked: `${config.http.base_url}/users/${user.id}/disliked`,
|
disliked: `${config.http.base_url}/users/${user.id}/disliked`,
|
||||||
featured: `${config.http.base_url}/users/${user.id}/featured`,
|
featured: `${config.http.base_url}/users/${user.id}/featured`,
|
||||||
liked: `${config.http.base_url}/users/${user.id}/liked`,
|
liked: `${config.http.base_url}/users/${user.id}/liked`,
|
||||||
followers: `${config.http.base_url}/users/${user.id}/followers`,
|
followers: `${config.http.base_url}/users/${user.id}/followers`,
|
||||||
following: `${config.http.base_url}/users/${user.id}/following`,
|
following: `${config.http.base_url}/users/${user.id}/following`,
|
||||||
inbox: `${config.http.base_url}/users/${user.id}/inbox`,
|
inbox: `${config.http.base_url}/users/${user.id}/inbox`,
|
||||||
outbox: `${config.http.base_url}/users/${user.id}/outbox`,
|
outbox: `${config.http.base_url}/users/${user.id}/outbox`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses mentions from a list of URIs
|
* Parses mentions from a list of URIs
|
||||||
*/
|
*/
|
||||||
export const parseMentionsUris = async (mentions: string[]) => {
|
export const parseMentionsUris = async (mentions: string[]) => {
|
||||||
return await client.user.findMany({
|
return await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
uri: {
|
uri: {
|
||||||
in: mentions,
|
in: mentions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -257,22 +259,22 @@ export const parseMentionsUris = async (mentions: string[]) => {
|
||||||
* @returns The user associated with the given access token.
|
* @returns The user associated with the given access token.
|
||||||
*/
|
*/
|
||||||
export const retrieveUserFromToken = async (access_token: string) => {
|
export const retrieveUserFromToken = async (access_token: string) => {
|
||||||
if (!access_token) return null;
|
if (!access_token) return null;
|
||||||
|
|
||||||
const token = await client.token.findFirst({
|
const token = await client.token.findFirst({
|
||||||
where: {
|
where: {
|
||||||
access_token,
|
access_token,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: {
|
user: {
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
|
|
||||||
return token.user;
|
return token.user;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -281,174 +283,174 @@ export const retrieveUserFromToken = async (access_token: string) => {
|
||||||
* @returns The relationship to the other user.
|
* @returns The relationship to the other user.
|
||||||
*/
|
*/
|
||||||
export const getRelationshipToOtherUser = async (
|
export const getRelationshipToOtherUser = async (
|
||||||
user: UserWithRelations,
|
user: UserWithRelations,
|
||||||
other: User
|
other: User,
|
||||||
) => {
|
) => {
|
||||||
return await client.relationship.findFirst({
|
return await client.relationship.findFirst({
|
||||||
where: {
|
where: {
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
subjectId: other.id,
|
subjectId: other.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates keys for the user.
|
* Generates keys for the user.
|
||||||
*/
|
*/
|
||||||
export const generateUserKeys = async () => {
|
export const generateUserKeys = async () => {
|
||||||
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
||||||
"sign",
|
"sign",
|
||||||
"verify",
|
"verify",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const privateKey = btoa(
|
const privateKey = btoa(
|
||||||
String.fromCharCode.apply(null, [
|
String.fromCharCode.apply(null, [
|
||||||
...new Uint8Array(
|
...new Uint8Array(
|
||||||
// jesus help me what do these letters mean
|
// jesus help me what do these letters mean
|
||||||
await crypto.subtle.exportKey("pkcs8", keys.privateKey)
|
await crypto.subtle.exportKey("pkcs8", keys.privateKey),
|
||||||
),
|
),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
const publicKey = btoa(
|
const publicKey = btoa(
|
||||||
String.fromCharCode(
|
String.fromCharCode(
|
||||||
...new Uint8Array(
|
...new Uint8Array(
|
||||||
// why is exporting a key so hard
|
// why is exporting a key so hard
|
||||||
await crypto.subtle.exportKey("spki", keys.publicKey)
|
await crypto.subtle.exportKey("spki", keys.publicKey),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add header, footer and newlines later on
|
// Add header, footer and newlines later on
|
||||||
// These keys are base64 encrypted
|
// These keys are base64 encrypted
|
||||||
return {
|
return {
|
||||||
private_key: privateKey,
|
private_key: privateKey,
|
||||||
public_key: publicKey,
|
public_key: publicKey,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userToAPI = (
|
export const userToAPI = (
|
||||||
user: UserWithRelations,
|
user: UserWithRelations,
|
||||||
isOwnAccount = false
|
isOwnAccount = false,
|
||||||
): APIAccount => {
|
): APIAccount => {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
display_name: user.displayName,
|
display_name: user.displayName,
|
||||||
note: user.note,
|
note: user.note,
|
||||||
url: user.uri,
|
url: user.uri,
|
||||||
avatar: getAvatarUrl(user, config),
|
avatar: getAvatarUrl(user, config),
|
||||||
header: getHeaderUrl(user, config),
|
header: getHeaderUrl(user, config),
|
||||||
locked: user.isLocked,
|
locked: user.isLocked,
|
||||||
created_at: new Date(user.createdAt).toISOString(),
|
created_at: new Date(user.createdAt).toISOString(),
|
||||||
followers_count: user.relationshipSubjects.filter(r => r.following)
|
followers_count: user.relationshipSubjects.filter((r) => r.following)
|
||||||
.length,
|
.length,
|
||||||
following_count: user.relationships.filter(r => r.following).length,
|
following_count: user.relationships.filter((r) => r.following).length,
|
||||||
statuses_count: user._count.statuses,
|
statuses_count: user._count.statuses,
|
||||||
emojis: user.emojis.map(emoji => emojiToAPI(emoji)),
|
emojis: user.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||||
// TODO: Add fields
|
// TODO: Add fields
|
||||||
fields: [],
|
fields: [],
|
||||||
bot: user.isBot,
|
bot: user.isBot,
|
||||||
source:
|
source:
|
||||||
isOwnAccount && user.source
|
isOwnAccount && user.source
|
||||||
? (user.source as APISource)
|
? (user.source as APISource)
|
||||||
: undefined,
|
: undefined,
|
||||||
// TODO: Add static avatar and header
|
// TODO: Add static avatar and header
|
||||||
avatar_static: "",
|
avatar_static: "",
|
||||||
header_static: "",
|
header_static: "",
|
||||||
acct:
|
acct:
|
||||||
user.instance === null
|
user.instance === null
|
||||||
? user.username
|
? user.username
|
||||||
: `${user.username}@${user.instance.base_url}`,
|
: `${user.username}@${user.instance.base_url}`,
|
||||||
// TODO: Add these fields
|
// TODO: Add these fields
|
||||||
limited: false,
|
limited: false,
|
||||||
moved: null,
|
moved: null,
|
||||||
noindex: false,
|
noindex: false,
|
||||||
suspended: false,
|
suspended: false,
|
||||||
discoverable: undefined,
|
discoverable: undefined,
|
||||||
mute_expires_at: undefined,
|
mute_expires_at: undefined,
|
||||||
group: false,
|
group: false,
|
||||||
pleroma: {
|
pleroma: {
|
||||||
is_admin: user.isAdmin,
|
is_admin: user.isAdmin,
|
||||||
is_moderator: user.isAdmin,
|
is_moderator: user.isAdmin,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should only return local users
|
* Should only return local users
|
||||||
*/
|
*/
|
||||||
export const userToLysand = (user: UserWithRelations): LysandUser => {
|
export const userToLysand = (user: UserWithRelations): LysandUser => {
|
||||||
if (user.instanceId !== null) {
|
if (user.instanceId !== null) {
|
||||||
throw new Error("Cannot convert remote user to Lysand format");
|
throw new Error("Cannot convert remote user to Lysand format");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
type: "User",
|
type: "User",
|
||||||
uri: user.uri,
|
uri: user.uri,
|
||||||
bio: [
|
bio: [
|
||||||
{
|
{
|
||||||
content: user.note,
|
content: user.note,
|
||||||
content_type: "text/html",
|
content_type: "text/html",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: htmlToText(user.note),
|
content: htmlToText(user.note),
|
||||||
content_type: "text/plain",
|
content_type: "text/plain",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
created_at: new Date(user.createdAt).toISOString(),
|
created_at: new Date(user.createdAt).toISOString(),
|
||||||
disliked: `${user.uri}/disliked`,
|
disliked: `${user.uri}/disliked`,
|
||||||
featured: `${user.uri}/featured`,
|
featured: `${user.uri}/featured`,
|
||||||
liked: `${user.uri}/liked`,
|
liked: `${user.uri}/liked`,
|
||||||
followers: `${user.uri}/followers`,
|
followers: `${user.uri}/followers`,
|
||||||
following: `${user.uri}/following`,
|
following: `${user.uri}/following`,
|
||||||
inbox: `${user.uri}/inbox`,
|
inbox: `${user.uri}/inbox`,
|
||||||
outbox: `${user.uri}/outbox`,
|
outbox: `${user.uri}/outbox`,
|
||||||
indexable: false,
|
indexable: false,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
avatar: [
|
avatar: [
|
||||||
{
|
{
|
||||||
content: getAvatarUrl(user, config) || "",
|
content: getAvatarUrl(user, config) || "",
|
||||||
content_type: `image/${user.avatar.split(".")[1]}`,
|
content_type: `image/${user.avatar.split(".")[1]}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
header: [
|
header: [
|
||||||
{
|
{
|
||||||
content: getHeaderUrl(user, config) || "",
|
content: getHeaderUrl(user, config) || "",
|
||||||
content_type: `image/${user.header.split(".")[1]}`,
|
content_type: `image/${user.header.split(".")[1]}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
display_name: user.displayName,
|
display_name: user.displayName,
|
||||||
fields: (user.source as any as APISource).fields.map(field => ({
|
fields: (user.source as APISource).fields.map((field) => ({
|
||||||
key: [
|
key: [
|
||||||
{
|
{
|
||||||
content: field.name,
|
content: field.name,
|
||||||
content_type: "text/html",
|
content_type: "text/html",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: htmlToText(field.name),
|
content: htmlToText(field.name),
|
||||||
content_type: "text/plain",
|
content_type: "text/plain",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
content: field.value,
|
content: field.value,
|
||||||
content_type: "text/html",
|
content_type: "text/html",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: htmlToText(field.value),
|
content: htmlToText(field.value),
|
||||||
content_type: "text/plain",
|
content_type: "text/plain",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})),
|
})),
|
||||||
public_key: {
|
public_key: {
|
||||||
actor: `${config.http.base_url}/users/${user.id}`,
|
actor: `${config.http.base_url}/users/${user.id}`,
|
||||||
public_key: user.publicKey,
|
public_key: user.publicKey,
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
"org.lysand:custom_emojis": {
|
"org.lysand:custom_emojis": {
|
||||||
emojis: user.emojis.map(emoji => emojiToLysand(emoji)),
|
emojis: user.emojis.map((emoji) => emojiToLysand(emoji)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,111 +1,111 @@
|
||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
export const userRelations: Prisma.UserInclude = {
|
export const userRelations: Prisma.UserInclude = {
|
||||||
emojis: true,
|
emojis: true,
|
||||||
instance: true,
|
instance: true,
|
||||||
likes: true,
|
likes: true,
|
||||||
relationships: true,
|
relationships: true,
|
||||||
relationshipSubjects: true,
|
relationshipSubjects: true,
|
||||||
pinnedNotes: true,
|
pinnedNotes: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
statuses: true,
|
statuses: true,
|
||||||
likes: true,
|
likes: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const statusAndUserRelations: Prisma.StatusInclude = {
|
export const statusAndUserRelations: Prisma.StatusInclude = {
|
||||||
author: {
|
author: {
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
},
|
},
|
||||||
application: true,
|
application: true,
|
||||||
emojis: true,
|
emojis: true,
|
||||||
inReplyToPost: {
|
inReplyToPost: {
|
||||||
include: {
|
include: {
|
||||||
author: {
|
author: {
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
},
|
},
|
||||||
application: true,
|
application: true,
|
||||||
emojis: true,
|
emojis: true,
|
||||||
inReplyToPost: {
|
inReplyToPost: {
|
||||||
include: {
|
include: {
|
||||||
author: true,
|
author: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
instance: true,
|
instance: true,
|
||||||
mentions: true,
|
mentions: true,
|
||||||
pinnedBy: true,
|
pinnedBy: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
replies: true,
|
replies: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reblogs: true,
|
reblogs: true,
|
||||||
attachments: true,
|
attachments: true,
|
||||||
instance: true,
|
instance: true,
|
||||||
mentions: {
|
mentions: {
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
},
|
},
|
||||||
pinnedBy: true,
|
pinnedBy: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
replies: true,
|
replies: true,
|
||||||
likes: true,
|
likes: true,
|
||||||
reblogs: true,
|
reblogs: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reblog: {
|
reblog: {
|
||||||
include: {
|
include: {
|
||||||
author: {
|
author: {
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
},
|
},
|
||||||
application: true,
|
application: true,
|
||||||
emojis: true,
|
emojis: true,
|
||||||
inReplyToPost: {
|
inReplyToPost: {
|
||||||
include: {
|
include: {
|
||||||
author: true,
|
author: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
instance: true,
|
instance: true,
|
||||||
mentions: {
|
mentions: {
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
},
|
},
|
||||||
pinnedBy: true,
|
pinnedBy: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
replies: true,
|
replies: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
quotingPost: {
|
quotingPost: {
|
||||||
include: {
|
include: {
|
||||||
author: {
|
author: {
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
},
|
},
|
||||||
application: true,
|
application: true,
|
||||||
emojis: true,
|
emojis: true,
|
||||||
inReplyToPost: {
|
inReplyToPost: {
|
||||||
include: {
|
include: {
|
||||||
author: true,
|
author: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
instance: true,
|
instance: true,
|
||||||
mentions: true,
|
mentions: true,
|
||||||
pinnedBy: true,
|
pinnedBy: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
replies: true,
|
replies: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
likes: {
|
likes: {
|
||||||
include: {
|
include: {
|
||||||
liker: true,
|
liker: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
56
index.ts
56
index.ts
|
|
@ -1,73 +1,75 @@
|
||||||
|
import { exists, mkdir } from "node:fs/promises";
|
||||||
|
import { connectMeili } from "@meilisearch";
|
||||||
|
import { moduleIsEntry } from "@module";
|
||||||
import type { PrismaClientInitializationError } from "@prisma/client/runtime/library";
|
import type { PrismaClientInitializationError } from "@prisma/client/runtime/library";
|
||||||
import { initializeRedisCache } from "@redis";
|
import { initializeRedisCache } from "@redis";
|
||||||
import { connectMeili } from "@meilisearch";
|
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
import { client } from "~database/datasource";
|
|
||||||
import { LogLevel, LogManager, MultiLogManager } from "log-manager";
|
import { LogLevel, LogManager, MultiLogManager } from "log-manager";
|
||||||
import { moduleIsEntry } from "@module";
|
import { client } from "~database/datasource";
|
||||||
import { createServer } from "~server";
|
import { createServer } from "~server";
|
||||||
import { exists, mkdir } from "fs/promises";
|
|
||||||
|
|
||||||
const timeAtStart = performance.now();
|
const timeAtStart = performance.now();
|
||||||
|
|
||||||
const requests_log = Bun.file(process.cwd() + "/logs/requests.log");
|
const requests_log = Bun.file(`${process.cwd()}/logs/requests.log`);
|
||||||
const isEntry = moduleIsEntry(import.meta.url);
|
const isEntry = moduleIsEntry(import.meta.url);
|
||||||
// If imported as a module, redirect logs to /dev/null to not pollute console (e.g. in tests)
|
// If imported as a module, redirect logs to /dev/null to not pollute console (e.g. in tests)
|
||||||
const logger = new LogManager(isEntry ? requests_log : Bun.file(`/dev/null`));
|
const logger = new LogManager(isEntry ? requests_log : Bun.file("/dev/null"));
|
||||||
const consoleLogger = new LogManager(
|
const consoleLogger = new LogManager(
|
||||||
isEntry ? Bun.stdout : Bun.file(`/dev/null`)
|
isEntry ? Bun.stdout : Bun.file("/dev/null"),
|
||||||
);
|
);
|
||||||
const dualLogger = new MultiLogManager([logger, consoleLogger]);
|
const dualLogger = new MultiLogManager([logger, consoleLogger]);
|
||||||
|
|
||||||
if (!(await exists(config.logging.storage.requests))) {
|
if (!(await exists(config.logging.storage.requests))) {
|
||||||
await consoleLogger.log(
|
await consoleLogger.log(
|
||||||
LogLevel.WARNING,
|
LogLevel.WARNING,
|
||||||
"Lysand",
|
"Lysand",
|
||||||
`Creating logs directory at ${process.cwd()}/logs/`
|
`Creating logs directory at ${process.cwd()}/logs/`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await mkdir(process.cwd() + "/logs/");
|
await mkdir(`${process.cwd()}/logs/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await dualLogger.log(LogLevel.INFO, "Lysand", "Starting Lysand...");
|
await dualLogger.log(LogLevel.INFO, "Lysand", "Starting Lysand...");
|
||||||
|
|
||||||
// NODE_ENV seems to be broken and output `development` even when set to production, so use the flag instead
|
// NODE_ENV seems to be broken and output `development` even when set to production, so use the flag instead
|
||||||
const isProd =
|
const isProd =
|
||||||
process.env.NODE_ENV === "production" || process.argv.includes("--prod");
|
process.env.NODE_ENV === "production" || process.argv.includes("--prod");
|
||||||
|
|
||||||
const redisCache = await initializeRedisCache();
|
const redisCache = await initializeRedisCache();
|
||||||
|
|
||||||
if (config.meilisearch.enabled) {
|
if (config.meilisearch.enabled) {
|
||||||
await connectMeili(dualLogger);
|
await connectMeili(dualLogger);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redisCache) {
|
if (redisCache) {
|
||||||
client.$use(redisCache);
|
client.$use(redisCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if database is reachable
|
// Check if database is reachable
|
||||||
let postCount = 0;
|
let postCount = 0;
|
||||||
try {
|
try {
|
||||||
postCount = await client.status.count();
|
postCount = await client.status.count();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e as PrismaClientInitializationError;
|
const error = e as PrismaClientInitializationError;
|
||||||
await logger.logError(LogLevel.CRITICAL, "Database", error);
|
await logger.logError(LogLevel.CRITICAL, "Database", error);
|
||||||
await consoleLogger.logError(LogLevel.CRITICAL, "Database", error);
|
await consoleLogger.logError(LogLevel.CRITICAL, "Database", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = createServer(config, dualLogger, isProd);
|
const server = createServer(config, dualLogger, isProd);
|
||||||
|
|
||||||
await dualLogger.log(
|
await dualLogger.log(
|
||||||
LogLevel.INFO,
|
LogLevel.INFO,
|
||||||
"Server",
|
"Server",
|
||||||
`Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`
|
`Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(
|
||||||
|
performance.now() - timeAtStart
|
||||||
|
).toFixed(0)}ms`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await dualLogger.log(
|
await dualLogger.log(
|
||||||
LogLevel.INFO,
|
LogLevel.INFO,
|
||||||
"Database",
|
"Database",
|
||||||
`Database is online, now serving ${postCount} posts`
|
`Database is online, now serving ${postCount} posts`,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { config, server };
|
export { config, server };
|
||||||
|
|
|
||||||
244
package.json
244
package.json
|
|
@ -1,130 +1,118 @@
|
||||||
{
|
{
|
||||||
"name": "lysand",
|
"name": "lysand",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"description": "A project to build a federated social network",
|
"description": "A project to build a federated social network",
|
||||||
"author": {
|
"author": {
|
||||||
"email": "contact@cpluspatch.com",
|
"email": "contact@cpluspatch.com",
|
||||||
"name": "CPlusPatch",
|
"name": "CPlusPatch",
|
||||||
"url": "https://cpluspatch.com"
|
"url": "https://cpluspatch.com"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/lysand-org/lysand/issues"
|
"url": "https://github.com/lysand-org/lysand/issues"
|
||||||
},
|
},
|
||||||
"icon": "https://github.com/lysand-org/lysand",
|
"icon": "https://github.com/lysand-org/lysand",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"keywords": [
|
"keywords": ["federated", "activitypub", "bun"],
|
||||||
"federated",
|
"workspaces": ["packages/*"],
|
||||||
"activitypub",
|
"maintainers": [
|
||||||
"bun"
|
{
|
||||||
],
|
"email": "contact@cpluspatch.com",
|
||||||
"workspaces": ["packages/*"],
|
"name": "CPlusPatch",
|
||||||
"maintainers": [
|
"url": "https://cpluspatch.com"
|
||||||
{
|
}
|
||||||
"email": "contact@cpluspatch.com",
|
],
|
||||||
"name": "CPlusPatch",
|
"repository": {
|
||||||
"url": "https://cpluspatch.com"
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/lysand-org/lysand.git"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --watch index.ts",
|
||||||
|
"vite:dev": "bunx --bun vite pages",
|
||||||
|
"vite:build": "bunx --bun vite build pages",
|
||||||
|
"start": "NODE_ENV=production bun run dist/index.js --prod",
|
||||||
|
"migrate-dev": "bun prisma migrate dev",
|
||||||
|
"migrate": "bun prisma migrate deploy",
|
||||||
|
"lint": "bunx --bun eslint --config .eslintrc.cjs --ext .ts .",
|
||||||
|
"prod-build": "bunx --bun vite build pages && bun run build.ts",
|
||||||
|
"prisma": "DATABASE_URL=$(bun run prisma.ts) bunx prisma",
|
||||||
|
"generate": "bun prisma generate",
|
||||||
|
"benchmark:timeline": "bun run benchmarks/timelines.ts",
|
||||||
|
"cloc": "cloc . --exclude-dir node_modules,dist",
|
||||||
|
"cli": "bun run cli.ts"
|
||||||
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@biomejs/biome",
|
||||||
|
"@prisma/client",
|
||||||
|
"@prisma/engines",
|
||||||
|
"esbuild",
|
||||||
|
"prisma",
|
||||||
|
"sharp"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "1.6.4",
|
||||||
|
"@julr/unocss-preset-forms": "^0.1.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",
|
||||||
|
"@unocss/cli": "latest",
|
||||||
|
"@vitejs/plugin-vue": "latest",
|
||||||
|
"@vueuse/head": "^2.0.0",
|
||||||
|
"activitypub-types": "^1.0.3",
|
||||||
|
"bun-types": "latest",
|
||||||
|
"typescript": "latest",
|
||||||
|
"unocss": "latest",
|
||||||
|
"untyped": "^1.4.2",
|
||||||
|
"vite": "latest",
|
||||||
|
"vite-ssr": "^0.17.1",
|
||||||
|
"vue": "^3.3.9",
|
||||||
|
"vue-router": "^4.2.5",
|
||||||
|
"vue-tsc": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.461.0",
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
|
"@json2csv/plainjs": "^7.0.6",
|
||||||
|
"@prisma/client": "^5.6.0",
|
||||||
|
"blurhash": "^2.0.5",
|
||||||
|
"bullmq": "latest",
|
||||||
|
"c12": "^1.10.0",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"cli-parser": "workspace:*",
|
||||||
|
"cli-table": "^0.3.11",
|
||||||
|
"config-manager": "workspace:*",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"extract-zip": "^2.0.1",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
|
"ioredis": "^5.3.2",
|
||||||
|
"ip-matching": "^2.1.2",
|
||||||
|
"iso-639-1": "^3.1.0",
|
||||||
|
"isomorphic-dompurify": "latest",
|
||||||
|
"jsonld": "^8.3.1",
|
||||||
|
"linkify-html": "^4.1.3",
|
||||||
|
"linkify-string": "^4.1.3",
|
||||||
|
"linkifyjs": "^4.1.3",
|
||||||
|
"log-manager": "workspace:*",
|
||||||
|
"marked": "latest",
|
||||||
|
"media-manager": "workspace:*",
|
||||||
|
"megalodon": "^10.0.0",
|
||||||
|
"meilisearch": "latest",
|
||||||
|
"merge-deep-ts": "^1.2.6",
|
||||||
|
"next-route-matcher": "^1.0.1",
|
||||||
|
"oauth4webapi": "^2.4.0",
|
||||||
|
"prisma": "^5.6.0",
|
||||||
|
"prisma-json-types-generator": "^3.0.4",
|
||||||
|
"prisma-redis-middleware": "^4.8.0",
|
||||||
|
"request-parser": "workspace:*",
|
||||||
|
"semver": "^7.5.4",
|
||||||
|
"sharp": "^0.33.0-rc.2",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/lysand-org/lysand.git"
|
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "bun run --watch index.ts",
|
|
||||||
"vite:dev": "bunx --bun vite pages",
|
|
||||||
"vite:build": "bunx --bun vite build pages",
|
|
||||||
"start": "NODE_ENV=production bun run dist/index.js --prod",
|
|
||||||
"migrate-dev": "bun prisma migrate dev",
|
|
||||||
"migrate": "bun prisma migrate deploy",
|
|
||||||
"lint": "bunx --bun eslint --config .eslintrc.cjs --ext .ts .",
|
|
||||||
"prod-build": "bunx --bun vite build pages && bun run build.ts",
|
|
||||||
"prisma": "DATABASE_URL=$(bun run prisma.ts) bunx prisma",
|
|
||||||
"generate": "bun prisma generate",
|
|
||||||
"benchmark:timeline": "bun run benchmarks/timelines.ts",
|
|
||||||
"cloc": "cloc . --exclude-dir node_modules,dist",
|
|
||||||
"cli": "bun run cli.ts"
|
|
||||||
},
|
|
||||||
"trustedDependencies": [
|
|
||||||
"@biomejs/biome",
|
|
||||||
"@prisma/client",
|
|
||||||
"@prisma/engines",
|
|
||||||
"esbuild",
|
|
||||||
"prisma",
|
|
||||||
"sharp"
|
|
||||||
],
|
|
||||||
"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",
|
|
||||||
"vite": "latest",
|
|
||||||
"vite-ssr": "^0.17.1",
|
|
||||||
"vue": "^3.3.9",
|
|
||||||
"vue-router": "^4.2.5",
|
|
||||||
"vue-tsc": "latest"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.3.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@aws-sdk/client-s3": "^3.461.0",
|
|
||||||
"@iarna/toml": "^2.2.5",
|
|
||||||
"@json2csv/plainjs": "^7.0.6",
|
|
||||||
"@prisma/client": "^5.6.0",
|
|
||||||
"blurhash": "^2.0.5",
|
|
||||||
"bullmq": "latest",
|
|
||||||
"c12": "^1.10.0",
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"cli-parser": "workspace:*",
|
|
||||||
"cli-table": "^0.3.11",
|
|
||||||
"config-manager": "workspace:*",
|
|
||||||
"eventemitter3": "^5.0.1",
|
|
||||||
"extract-zip": "^2.0.1",
|
|
||||||
"html-to-text": "^9.0.5",
|
|
||||||
"ioredis": "^5.3.2",
|
|
||||||
"ip-matching": "^2.1.2",
|
|
||||||
"iso-639-1": "^3.1.0",
|
|
||||||
"isomorphic-dompurify": "latest",
|
|
||||||
"jsonld": "^8.3.1",
|
|
||||||
"linkify-html": "^4.1.3",
|
|
||||||
"linkify-string": "^4.1.3",
|
|
||||||
"linkifyjs": "^4.1.3",
|
|
||||||
"log-manager": "workspace:*",
|
|
||||||
"marked": "latest",
|
|
||||||
"media-manager": "workspace:*",
|
|
||||||
"megalodon": "^10.0.0",
|
|
||||||
"meilisearch": "latest",
|
|
||||||
"merge-deep-ts": "^1.2.6",
|
|
||||||
"next-route-matcher": "^1.0.1",
|
|
||||||
"oauth4webapi": "^2.4.0",
|
|
||||||
"prisma": "^5.6.0",
|
|
||||||
"prisma-json-types-generator": "^3.0.4",
|
|
||||||
"prisma-redis-middleware": "^4.8.0",
|
|
||||||
"request-parser": "workspace:*",
|
|
||||||
"semver": "^7.5.4",
|
|
||||||
"sharp": "^0.33.0-rc.2",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
export interface CliParameter {
|
export interface CliParameter {
|
||||||
name: string;
|
name: string;
|
||||||
/* Like -v for --version */
|
/* Like -v for --version */
|
||||||
shortName?: string;
|
shortName?: string;
|
||||||
/**
|
/**
|
||||||
* If not positioned, the argument will need to be called with --name value instead of just value
|
* If not positioned, the argument will need to be called with --name value instead of just value
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
positioned?: boolean;
|
positioned?: boolean;
|
||||||
/* Whether the argument needs a value (requires positioned to be false) */
|
/* Whether the argument needs a value (requires positioned to be false) */
|
||||||
needsValue?: boolean;
|
needsValue?: boolean;
|
||||||
optional?: true;
|
optional?: true;
|
||||||
type: CliParameterType;
|
type: CliParameterType;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CliParameterType {
|
export enum CliParameterType {
|
||||||
STRING = "string",
|
STRING = "string",
|
||||||
NUMBER = "number",
|
NUMBER = "number",
|
||||||
BOOLEAN = "boolean",
|
BOOLEAN = "boolean",
|
||||||
ARRAY = "array",
|
ARRAY = "array",
|
||||||
EMPTY = "empty",
|
EMPTY = "empty",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { CliParameterType, type CliParameter } from "./cli-builder.type";
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import strip from "strip-ansi";
|
import strip from "strip-ansi";
|
||||||
|
import { type CliParameter, CliParameterType } from "./cli-builder.type";
|
||||||
|
|
||||||
export function startsWithArray(fullArray: any[], startArray: any[]) {
|
export function startsWithArray(fullArray: string[], startArray: string[]) {
|
||||||
if (startArray.length > fullArray.length) {
|
if (startArray.length > fullArray.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return fullArray
|
return fullArray
|
||||||
.slice(0, startArray.length)
|
.slice(0, startArray.length)
|
||||||
.every((value, index) => value === startArray[index]);
|
.every((value, index) => value === startArray[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TreeType {
|
interface TreeType {
|
||||||
[key: string]: CliCommand | TreeType;
|
[key: string]: CliCommand | TreeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -20,178 +20,186 @@ interface TreeType {
|
||||||
* @param commands Array of commands to register
|
* @param commands Array of commands to register
|
||||||
*/
|
*/
|
||||||
export class CliBuilder {
|
export class CliBuilder {
|
||||||
constructor(public commands: CliCommand[] = []) {}
|
constructor(public commands: CliCommand[] = []) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add command to the CLI
|
* Add command to the CLI
|
||||||
* @throws Error if command already exists
|
* @throws Error if command already exists
|
||||||
* @param command Command to add
|
* @param command Command to add
|
||||||
*/
|
*/
|
||||||
registerCommand(command: CliCommand) {
|
registerCommand(command: CliCommand) {
|
||||||
if (this.checkIfCommandAlreadyExists(command)) {
|
if (this.checkIfCommandAlreadyExists(command)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Command category '${command.categories.join(" ")}' already exists`
|
`Command category '${command.categories.join(
|
||||||
);
|
" ",
|
||||||
}
|
)}' already exists`,
|
||||||
this.commands.push(command);
|
);
|
||||||
}
|
}
|
||||||
|
this.commands.push(command);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add multiple commands to the CLI
|
* Add multiple commands to the CLI
|
||||||
* @throws Error if command already exists
|
* @throws Error if command already exists
|
||||||
* @param commands Commands to add
|
* @param commands Commands to add
|
||||||
*/
|
*/
|
||||||
registerCommands(commands: CliCommand[]) {
|
registerCommands(commands: CliCommand[]) {
|
||||||
const existingCommand = commands.find(command =>
|
const existingCommand = commands.find((command) =>
|
||||||
this.checkIfCommandAlreadyExists(command)
|
this.checkIfCommandAlreadyExists(command),
|
||||||
);
|
);
|
||||||
if (existingCommand) {
|
if (existingCommand) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Command category '${existingCommand.categories.join(" ")}' already exists`
|
`Command category '${existingCommand.categories.join(
|
||||||
);
|
" ",
|
||||||
}
|
)}' already exists`,
|
||||||
this.commands.push(...commands);
|
);
|
||||||
}
|
}
|
||||||
|
this.commands.push(...commands);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove command from the CLI
|
* Remove command from the CLI
|
||||||
* @param command Command to remove
|
* @param command Command to remove
|
||||||
*/
|
*/
|
||||||
deregisterCommand(command: CliCommand) {
|
deregisterCommand(command: CliCommand) {
|
||||||
this.commands = this.commands.filter(
|
this.commands = this.commands.filter(
|
||||||
registeredCommand => registeredCommand !== command
|
(registeredCommand) => registeredCommand !== command,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove multiple commands from the CLI
|
* Remove multiple commands from the CLI
|
||||||
* @param commands Commands to remove
|
* @param commands Commands to remove
|
||||||
*/
|
*/
|
||||||
deregisterCommands(commands: CliCommand[]) {
|
deregisterCommands(commands: CliCommand[]) {
|
||||||
this.commands = this.commands.filter(
|
this.commands = this.commands.filter(
|
||||||
registeredCommand => !commands.includes(registeredCommand)
|
(registeredCommand) => !commands.includes(registeredCommand),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIfCommandAlreadyExists(command: CliCommand) {
|
checkIfCommandAlreadyExists(command: CliCommand) {
|
||||||
return this.commands.some(
|
return this.commands.some(
|
||||||
registeredCommand =>
|
(registeredCommand) =>
|
||||||
registeredCommand.categories.length ==
|
registeredCommand.categories.length ===
|
||||||
command.categories.length &&
|
command.categories.length &&
|
||||||
registeredCommand.categories.every(
|
registeredCommand.categories.every(
|
||||||
(category, index) => category === command.categories[index]
|
(category, index) => category === command.categories[index],
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get relevant args for the command (without executable or runtime)
|
* Get relevant args for the command (without executable or runtime)
|
||||||
* @param args Arguments passed to the CLI
|
* @param args Arguments passed to the CLI
|
||||||
*/
|
*/
|
||||||
private getRelevantArgs(args: string[]) {
|
private getRelevantArgs(args: string[]) {
|
||||||
if (args[0].startsWith("./")) {
|
if (args[0].startsWith("./")) {
|
||||||
// Formatted like ./cli.ts [command]
|
// Formatted like ./cli.ts [command]
|
||||||
return args.slice(1);
|
return args.slice(1);
|
||||||
} else if (args[0].includes("bun")) {
|
}
|
||||||
// Formatted like bun cli.ts [command]
|
if (args[0].includes("bun")) {
|
||||||
return args.slice(2);
|
// Formatted like bun cli.ts [command]
|
||||||
} else {
|
return args.slice(2);
|
||||||
return args;
|
}
|
||||||
}
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn raw system args into a CLI command and run it
|
* Turn raw system args into a CLI command and run it
|
||||||
* @param args Args directly from process.argv
|
* @param args Args directly from process.argv
|
||||||
*/
|
*/
|
||||||
async processArgs(args: string[]) {
|
async processArgs(args: string[]) {
|
||||||
const revelantArgs = this.getRelevantArgs(args);
|
const revelantArgs = this.getRelevantArgs(args);
|
||||||
|
|
||||||
// Handle "-h", "--help" and "help" commands as special cases
|
// Handle "-h", "--help" and "help" commands as special cases
|
||||||
if (revelantArgs.length === 1) {
|
if (revelantArgs.length === 1) {
|
||||||
if (["-h", "--help", "help"].includes(revelantArgs[0])) {
|
if (["-h", "--help", "help"].includes(revelantArgs[0])) {
|
||||||
this.displayHelp();
|
this.displayHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find revelant command
|
// Find revelant command
|
||||||
// Search for a command with as many categories matching args as possible
|
// Search for a command with as many categories matching args as possible
|
||||||
const matchingCommands = this.commands.filter(command =>
|
const matchingCommands = this.commands.filter((command) =>
|
||||||
startsWithArray(revelantArgs, command.categories)
|
startsWithArray(revelantArgs, command.categories),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (matchingCommands.length === 0) {
|
if (matchingCommands.length === 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`Invalid command "${revelantArgs.join(" ")}". Please use the ${chalk.bold("help")} command to see a list of commands`
|
`Invalid command "${revelantArgs.join(
|
||||||
);
|
" ",
|
||||||
return 0;
|
)}". Please use the ${chalk.bold(
|
||||||
}
|
"help",
|
||||||
|
)} command to see a list of commands`,
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Get command with largest category size
|
// Get command with largest category size
|
||||||
const command = matchingCommands.reduce((prev, current) =>
|
const command = matchingCommands.reduce((prev, current) =>
|
||||||
prev.categories.length > current.categories.length ? prev : current
|
prev.categories.length > current.categories.length ? prev : current,
|
||||||
);
|
);
|
||||||
|
|
||||||
const argsWithoutCategories = revelantArgs.slice(
|
const argsWithoutCategories = revelantArgs.slice(
|
||||||
command.categories.length
|
command.categories.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
return await command.run(argsWithoutCategories);
|
return await command.run(argsWithoutCategories);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively urns the commands into a tree where subcategories mark each sub-branch
|
* Recursively urns the commands into a tree where subcategories mark each sub-branch
|
||||||
* @example
|
* @example
|
||||||
* ```txt
|
* ```txt
|
||||||
* user verify
|
* user verify
|
||||||
* user delete
|
* user delete
|
||||||
* user new admin
|
* user new admin
|
||||||
* user new
|
* user new
|
||||||
* ->
|
* ->
|
||||||
* user
|
* user
|
||||||
* verify
|
* verify
|
||||||
* delete
|
* delete
|
||||||
* new
|
* new
|
||||||
* admin
|
* admin
|
||||||
* ""
|
* ""
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
getCommandTree(commands: CliCommand[]): TreeType {
|
getCommandTree(commands: CliCommand[]): TreeType {
|
||||||
const tree: TreeType = {};
|
const tree: TreeType = {};
|
||||||
|
|
||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
let currentLevel = tree; // Start at the root
|
let currentLevel = tree; // Start at the root
|
||||||
|
|
||||||
// Split the command into parts and iterate over them
|
// Split the command into parts and iterate over them
|
||||||
for (const part of command.categories) {
|
for (const part of command.categories) {
|
||||||
// If this part doesn't exist in the current level of the tree, add it (__proto__ check to prevent prototype pollution)
|
// If this part doesn't exist in the current level of the tree, add it (__proto__ check to prevent prototype pollution)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (!currentLevel[part] && part !== "__proto__") {
|
if (!currentLevel[part] && part !== "__proto__") {
|
||||||
// If this is the last part of the command, add the command itself
|
// If this is the last part of the command, add the command itself
|
||||||
if (
|
if (
|
||||||
part ===
|
part ===
|
||||||
command.categories[command.categories.length - 1]
|
command.categories[command.categories.length - 1]
|
||||||
) {
|
) {
|
||||||
currentLevel[part] = command;
|
currentLevel[part] = command;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
currentLevel[part] = {};
|
currentLevel[part] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move down to the next level of the tree
|
// Move down to the next level of the tree
|
||||||
currentLevel = currentLevel[part] as TreeType;
|
currentLevel = currentLevel[part] as TreeType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display help for every command in a tree manner
|
* Display help for every command in a tree manner
|
||||||
*/
|
*/
|
||||||
displayHelp() {
|
displayHelp() {
|
||||||
/*
|
/*
|
||||||
user
|
user
|
||||||
set
|
set
|
||||||
admin: List of admin commands
|
admin: List of admin commands
|
||||||
|
|
@ -204,217 +212,242 @@ export class CliBuilder {
|
||||||
verify
|
verify
|
||||||
...
|
...
|
||||||
*/
|
*/
|
||||||
const tree = this.getCommandTree(this.commands);
|
const tree = this.getCommandTree(this.commands);
|
||||||
let writeBuffer = "";
|
let writeBuffer = "";
|
||||||
|
|
||||||
const displayTree = (tree: TreeType, depth = 0) => {
|
const displayTree = (tree: TreeType, depth = 0) => {
|
||||||
for (const [key, value] of Object.entries(tree)) {
|
for (const [key, value] of Object.entries(tree)) {
|
||||||
if (value instanceof CliCommand) {
|
if (value instanceof CliCommand) {
|
||||||
writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}|${chalk.underline(value.description)}\n`;
|
writeBuffer += `${" ".repeat(depth)}${chalk.blue(
|
||||||
const positionedArgs = value.argTypes.filter(
|
key,
|
||||||
arg => arg.positioned ?? true
|
)}|${chalk.underline(value.description)}\n`;
|
||||||
);
|
const positionedArgs = value.argTypes.filter(
|
||||||
const unpositionedArgs = value.argTypes.filter(
|
(arg) => arg.positioned ?? true,
|
||||||
arg => !(arg.positioned ?? true)
|
);
|
||||||
);
|
const unpositionedArgs = value.argTypes.filter(
|
||||||
|
(arg) => !(arg.positioned ?? true),
|
||||||
|
);
|
||||||
|
|
||||||
for (const arg of positionedArgs) {
|
for (const arg of positionedArgs) {
|
||||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.green(
|
writeBuffer += `${" ".repeat(
|
||||||
arg.name
|
depth + 1,
|
||||||
)}|${
|
)}${chalk.green(arg.name)}|${
|
||||||
arg.description ?? "(no description)"
|
arg.description ?? "(no description)"
|
||||||
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
|
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
|
||||||
}
|
}
|
||||||
for (const arg of unpositionedArgs) {
|
for (const arg of unpositionedArgs) {
|
||||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}${arg.shortName ? ", " + chalk.yellow("-" + arg.shortName) : ""}|${
|
writeBuffer += `${" ".repeat(
|
||||||
arg.description ?? "(no description)"
|
depth + 1,
|
||||||
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
|
)}${chalk.yellow(`--${arg.name}`)}${
|
||||||
}
|
arg.shortName
|
||||||
|
? `, ${chalk.yellow(`-${arg.shortName}`)}`
|
||||||
|
: ""
|
||||||
|
}|${arg.description ?? "(no description)"} ${
|
||||||
|
arg.optional ? chalk.gray("(optional)") : ""
|
||||||
|
}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
if (value.example) {
|
if (value.example) {
|
||||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.bold("Example:")} ${chalk.bgGray(
|
writeBuffer += `${" ".repeat(depth + 1)}${chalk.bold(
|
||||||
value.example
|
"Example:",
|
||||||
)}\n`;
|
)} ${chalk.bgGray(value.example)}\n`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}\n`;
|
writeBuffer += `${" ".repeat(depth)}${chalk.blue(
|
||||||
displayTree(value, depth + 1);
|
key,
|
||||||
}
|
)}\n`;
|
||||||
}
|
displayTree(value, depth + 1);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
displayTree(tree);
|
displayTree(tree);
|
||||||
|
|
||||||
// Replace all "|" with enough dots so that the text on the left + the dots = the same length
|
// Replace all "|" with enough dots so that the text on the left + the dots = the same length
|
||||||
const optimal_length = Number(
|
const optimal_length = Number(
|
||||||
// @ts-expect-error Slightly hacky but works
|
writeBuffer
|
||||||
writeBuffer.split("\n").reduce((prev, current) => {
|
.split("\n")
|
||||||
// If previousValue is empty
|
// @ts-expect-error I don't know how this works and I don't want to know
|
||||||
if (!prev)
|
.reduce((prev, current) => {
|
||||||
return current.includes("|")
|
// If previousValue is empty
|
||||||
? current.split("|")[0].length
|
if (!prev)
|
||||||
: 0;
|
return current.includes("|")
|
||||||
if (!current.includes("|")) return prev;
|
? current.split("|")[0].length
|
||||||
const [left] = current.split("|");
|
: 0;
|
||||||
// Strip ANSI color codes or they mess up the length
|
if (!current.includes("|")) return prev;
|
||||||
return Math.max(Number(prev), strip(left).length);
|
const [left] = current.split("|");
|
||||||
})
|
// Strip ANSI color codes or they mess up the length
|
||||||
);
|
return Math.max(Number(prev), Bun.stringWidth(left));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
for (const line of writeBuffer.split("\n")) {
|
for (const line of writeBuffer.split("\n")) {
|
||||||
const [left, right] = line.split("|");
|
const [left, right] = line.split("|");
|
||||||
if (!right) {
|
if (!right) {
|
||||||
console.log(left);
|
console.log(left);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Strip ANSI color codes or they mess up the length
|
// Strip ANSI color codes or they mess up the length
|
||||||
const dots = ".".repeat(optimal_length + 5 - strip(left).length);
|
const dots = ".".repeat(optimal_length + 5 - Bun.stringWidth(left));
|
||||||
console.log(`${left}${dots}${right}`);
|
console.log(`${left}${dots}${right}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExecuteFunction<T> = (
|
type ExecuteFunction<T> = (
|
||||||
instance: CliCommand,
|
instance: CliCommand,
|
||||||
args: Partial<T>
|
args: Partial<T>,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||||
) => Promise<number> | Promise<void> | number | void;
|
) => Promise<number> | Promise<void> | number | void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command that can be executed from the command line
|
* A command that can be executed from the command line
|
||||||
* @param categories Example: `["user", "create"]` for the command `./cli user create --name John`
|
* @param categories Example: `["user", "create"]` for the command `./cli user create --name John`
|
||||||
*/
|
*/
|
||||||
export class CliCommand<T = any> {
|
|
||||||
constructor(
|
|
||||||
public categories: string[],
|
|
||||||
public argTypes: CliParameter[],
|
|
||||||
private execute: ExecuteFunction<T>,
|
|
||||||
public description?: string,
|
|
||||||
public example?: string
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
* Display help message for the command
|
export class CliCommand<T = any> {
|
||||||
* formatted with Chalk and with emojis
|
constructor(
|
||||||
*/
|
public categories: string[],
|
||||||
displayHelp() {
|
public argTypes: CliParameter[],
|
||||||
const positionedArgs = this.argTypes.filter(
|
private execute: ExecuteFunction<T>,
|
||||||
arg => arg.positioned ?? true
|
public description?: string,
|
||||||
);
|
public example?: string,
|
||||||
const unpositionedArgs = this.argTypes.filter(
|
) {}
|
||||||
arg => !(arg.positioned ?? true)
|
|
||||||
);
|
/**
|
||||||
const helpMessage = `
|
* Display help message for the command
|
||||||
|
* formatted with Chalk and with emojis
|
||||||
|
*/
|
||||||
|
displayHelp() {
|
||||||
|
const positionedArgs = this.argTypes.filter(
|
||||||
|
(arg) => arg.positioned ?? true,
|
||||||
|
);
|
||||||
|
const unpositionedArgs = this.argTypes.filter(
|
||||||
|
(arg) => !(arg.positioned ?? true),
|
||||||
|
);
|
||||||
|
const helpMessage = `
|
||||||
${chalk.green("📚 Command:")} ${chalk.yellow(this.categories.join(" "))}
|
${chalk.green("📚 Command:")} ${chalk.yellow(this.categories.join(" "))}
|
||||||
${this.description ? `${chalk.cyan(this.description)}\n` : ""}
|
${this.description ? `${chalk.cyan(this.description)}\n` : ""}
|
||||||
${chalk.magenta("🔧 Arguments:")}
|
${chalk.magenta("🔧 Arguments:")}
|
||||||
${positionedArgs
|
${positionedArgs
|
||||||
.map(
|
.map(
|
||||||
arg =>
|
(arg) =>
|
||||||
`${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${
|
`${chalk.bold(arg.name)}: ${chalk.blue(
|
||||||
arg.optional ? chalk.gray("(optional)") : ""
|
arg.description ?? "(no description)",
|
||||||
}`
|
)} ${arg.optional ? chalk.gray("(optional)") : ""}`,
|
||||||
)
|
)
|
||||||
.join("\n")}
|
.join("\n")}
|
||||||
${unpositionedArgs
|
${unpositionedArgs
|
||||||
.map(
|
.map(
|
||||||
arg =>
|
(arg) =>
|
||||||
`--${chalk.bold(arg.name)}${arg.shortName ? `, -${arg.shortName}` : ""}: ${chalk.blue(arg.description ?? "(no description)")} ${
|
`--${chalk.bold(arg.name)}${
|
||||||
arg.optional ? chalk.gray("(optional)") : ""
|
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);
|
console.log(helpMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses string array arguments into a full JavaScript object
|
* Parses string array arguments into a full JavaScript object
|
||||||
* @param argsWithoutCategories
|
* @param argsWithoutCategories
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
private parseArgs(argsWithoutCategories: string[]): Record<string, any> {
|
private parseArgs(
|
||||||
const parsedArgs: Record<string, any> = {};
|
argsWithoutCategories: string[],
|
||||||
let currentParameter: CliParameter | null = null;
|
): 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++) {
|
for (let i = 0; i < argsWithoutCategories.length; i++) {
|
||||||
const arg = argsWithoutCategories[i];
|
const arg = argsWithoutCategories[i];
|
||||||
|
|
||||||
if (arg.startsWith("--")) {
|
if (arg.startsWith("--")) {
|
||||||
const argName = arg.substring(2);
|
const argName = arg.substring(2);
|
||||||
currentParameter =
|
currentParameter =
|
||||||
this.argTypes.find(argType => argType.name === argName) ||
|
this.argTypes.find((argType) => argType.name === argName) ||
|
||||||
null;
|
null;
|
||||||
if (currentParameter && !currentParameter.needsValue) {
|
if (currentParameter && !currentParameter.needsValue) {
|
||||||
parsedArgs[argName] = true;
|
parsedArgs[argName] = true;
|
||||||
currentParameter = null;
|
currentParameter = null;
|
||||||
} else if (currentParameter && currentParameter.needsValue) {
|
} else if (currentParameter?.needsValue) {
|
||||||
parsedArgs[argName] = this.castArgValue(
|
parsedArgs[argName] = this.castArgValue(
|
||||||
argsWithoutCategories[i + 1],
|
argsWithoutCategories[i + 1],
|
||||||
currentParameter.type
|
currentParameter.type,
|
||||||
);
|
);
|
||||||
i++;
|
i++;
|
||||||
currentParameter = null;
|
currentParameter = null;
|
||||||
}
|
}
|
||||||
} else if (arg.startsWith("-")) {
|
} else if (arg.startsWith("-")) {
|
||||||
const shortName = arg.substring(1);
|
const shortName = arg.substring(1);
|
||||||
const argType = this.argTypes.find(
|
const argType = this.argTypes.find(
|
||||||
argType => argType.shortName === shortName
|
(argType) => argType.shortName === shortName,
|
||||||
);
|
);
|
||||||
if (argType && !argType.needsValue) {
|
if (argType && !argType.needsValue) {
|
||||||
parsedArgs[argType.name] = true;
|
parsedArgs[argType.name] = true;
|
||||||
} else if (argType && argType.needsValue) {
|
} else if (argType?.needsValue) {
|
||||||
parsedArgs[argType.name] = this.castArgValue(
|
parsedArgs[argType.name] = this.castArgValue(
|
||||||
argsWithoutCategories[i + 1],
|
argsWithoutCategories[i + 1],
|
||||||
argType.type
|
argType.type,
|
||||||
);
|
);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
} else if (currentParameter) {
|
} else if (currentParameter) {
|
||||||
parsedArgs[currentParameter.name] = this.castArgValue(
|
parsedArgs[currentParameter.name] = this.castArgValue(
|
||||||
arg,
|
arg,
|
||||||
currentParameter.type
|
currentParameter.type,
|
||||||
);
|
);
|
||||||
currentParameter = null;
|
currentParameter = null;
|
||||||
} else {
|
} else {
|
||||||
const positionedArgType = this.argTypes.find(
|
const positionedArgType = this.argTypes.find(
|
||||||
argType => argType.positioned && !parsedArgs[argType.name]
|
(argType) =>
|
||||||
);
|
argType.positioned && !parsedArgs[argType.name],
|
||||||
if (positionedArgType) {
|
);
|
||||||
parsedArgs[positionedArgType.name] = this.castArgValue(
|
if (positionedArgType) {
|
||||||
arg,
|
parsedArgs[positionedArgType.name] = this.castArgValue(
|
||||||
positionedArgType.type
|
arg,
|
||||||
);
|
positionedArgType.type,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return parsedArgs;
|
return parsedArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private castArgValue(value: string, type: CliParameter["type"]): any {
|
private castArgValue(
|
||||||
switch (type) {
|
value: string,
|
||||||
case CliParameterType.STRING:
|
type: CliParameter["type"],
|
||||||
return value;
|
): string | number | boolean | string[] {
|
||||||
case CliParameterType.NUMBER:
|
switch (type) {
|
||||||
return Number(value);
|
case CliParameterType.STRING:
|
||||||
case CliParameterType.BOOLEAN:
|
return value;
|
||||||
return value === "true";
|
case CliParameterType.NUMBER:
|
||||||
case CliParameterType.ARRAY:
|
return Number(value);
|
||||||
return value.split(",");
|
case CliParameterType.BOOLEAN:
|
||||||
default:
|
return value === "true";
|
||||||
return value;
|
case CliParameterType.ARRAY:
|
||||||
}
|
return value.split(",");
|
||||||
}
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the execute function with the parsed parameters as an argument
|
* Runs the execute function with the parsed parameters as an argument
|
||||||
*/
|
*/
|
||||||
async run(argsWithoutCategories: string[]) {
|
async run(argsWithoutCategories: string[]) {
|
||||||
const args = this.parseArgs(argsWithoutCategories);
|
const args = this.parseArgs(argsWithoutCategories);
|
||||||
return await this.execute(this, args as any);
|
return await this.execute(this, args as T);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "cli-parser",
|
"name": "cli-parser",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": { "chalk": "^5.3.0", "strip-ansi": "^7.1.0" }
|
"dependencies": { "chalk": "^5.3.0", "strip-ansi": "^7.1.0" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,485 +1,488 @@
|
||||||
// FILEPATH: /home/jessew/Dev/lysand/packages/cli-parser/index.test.ts
|
import { beforeEach, describe, expect, it, jest, spyOn } from "bun:test";
|
||||||
import { CliCommand, CliBuilder, startsWithArray } from "..";
|
|
||||||
import { describe, beforeEach, it, expect, jest, spyOn } from "bun:test";
|
|
||||||
import stripAnsi from "strip-ansi";
|
import stripAnsi from "strip-ansi";
|
||||||
|
// FILEPATH: /home/jessew/Dev/lysand/packages/cli-parser/index.test.ts
|
||||||
|
import { CliBuilder, CliCommand, startsWithArray } from "..";
|
||||||
import { CliParameterType } from "../cli-builder.type";
|
import { CliParameterType } from "../cli-builder.type";
|
||||||
|
|
||||||
describe("startsWithArray", () => {
|
describe("startsWithArray", () => {
|
||||||
it("should return true when fullArray starts with startArray", () => {
|
it("should return true when fullArray starts with startArray", () => {
|
||||||
const fullArray = ["a", "b", "c", "d", "e"];
|
const fullArray = ["a", "b", "c", "d", "e"];
|
||||||
const startArray = ["a", "b", "c"];
|
const startArray = ["a", "b", "c"];
|
||||||
expect(startsWithArray(fullArray, startArray)).toBe(true);
|
expect(startsWithArray(fullArray, startArray)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false when fullArray does not start with startArray", () => {
|
it("should return false when fullArray does not start with startArray", () => {
|
||||||
const fullArray = ["a", "b", "c", "d", "e"];
|
const fullArray = ["a", "b", "c", "d", "e"];
|
||||||
const startArray = ["b", "c", "d"];
|
const startArray = ["b", "c", "d"];
|
||||||
expect(startsWithArray(fullArray, startArray)).toBe(false);
|
expect(startsWithArray(fullArray, startArray)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when startArray is empty", () => {
|
it("should return true when startArray is empty", () => {
|
||||||
const fullArray = ["a", "b", "c", "d", "e"];
|
const fullArray = ["a", "b", "c", "d", "e"];
|
||||||
const startArray: any[] = [];
|
const startArray: string[] = [];
|
||||||
expect(startsWithArray(fullArray, startArray)).toBe(true);
|
expect(startsWithArray(fullArray, startArray)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false when fullArray is shorter than startArray", () => {
|
it("should return false when fullArray is shorter than startArray", () => {
|
||||||
const fullArray = ["a", "b", "c"];
|
const fullArray = ["a", "b", "c"];
|
||||||
const startArray = ["a", "b", "c", "d", "e"];
|
const startArray = ["a", "b", "c", "d", "e"];
|
||||||
expect(startsWithArray(fullArray, startArray)).toBe(false);
|
expect(startsWithArray(fullArray, startArray)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("CliCommand", () => {
|
describe("CliCommand", () => {
|
||||||
let cliCommand: CliCommand;
|
let cliCommand: CliCommand;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cliCommand = new CliCommand(
|
cliCommand = new CliCommand(
|
||||||
["category1", "category2"],
|
["category1", "category2"],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "arg1",
|
name: "arg1",
|
||||||
type: CliParameterType.STRING,
|
type: CliParameterType.STRING,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg2",
|
name: "arg2",
|
||||||
shortName: "a",
|
shortName: "a",
|
||||||
type: CliParameterType.NUMBER,
|
type: CliParameterType.NUMBER,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg3",
|
name: "arg3",
|
||||||
type: CliParameterType.BOOLEAN,
|
type: CliParameterType.BOOLEAN,
|
||||||
needsValue: false,
|
needsValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg4",
|
name: "arg4",
|
||||||
type: CliParameterType.ARRAY,
|
type: CliParameterType.ARRAY,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse string arguments correctly", () => {
|
it("should parse string arguments correctly", () => {
|
||||||
const args = cliCommand["parseArgs"]([
|
// @ts-expect-error Testing private method
|
||||||
"--arg1",
|
const args = cliCommand.parseArgs([
|
||||||
"value1",
|
"--arg1",
|
||||||
"--arg2",
|
"value1",
|
||||||
"42",
|
"--arg2",
|
||||||
"--arg3",
|
"42",
|
||||||
"--arg4",
|
"--arg3",
|
||||||
"value1,value2",
|
"--arg4",
|
||||||
]);
|
"value1,value2",
|
||||||
expect(args).toEqual({
|
]);
|
||||||
arg1: "value1",
|
expect(args).toEqual({
|
||||||
arg2: 42,
|
arg1: "value1",
|
||||||
arg3: true,
|
arg2: 42,
|
||||||
arg4: ["value1", "value2"],
|
arg3: true,
|
||||||
});
|
arg4: ["value1", "value2"],
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should parse short names for arguments too", () => {
|
it("should parse short names for arguments too", () => {
|
||||||
const args = cliCommand["parseArgs"]([
|
// @ts-expect-error Testing private method
|
||||||
"--arg1",
|
const args = cliCommand.parseArgs([
|
||||||
"value1",
|
"--arg1",
|
||||||
"-a",
|
"value1",
|
||||||
"42",
|
"-a",
|
||||||
"--arg3",
|
"42",
|
||||||
"--arg4",
|
"--arg3",
|
||||||
"value1,value2",
|
"--arg4",
|
||||||
]);
|
"value1,value2",
|
||||||
expect(args).toEqual({
|
]);
|
||||||
arg1: "value1",
|
expect(args).toEqual({
|
||||||
arg2: 42,
|
arg1: "value1",
|
||||||
arg3: true,
|
arg2: 42,
|
||||||
arg4: ["value1", "value2"],
|
arg3: true,
|
||||||
});
|
arg4: ["value1", "value2"],
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should cast argument values correctly", () => {
|
it("should cast argument values correctly", () => {
|
||||||
expect(cliCommand["castArgValue"]("42", CliParameterType.NUMBER)).toBe(
|
// @ts-expect-error Testing private method
|
||||||
42
|
expect(cliCommand.castArgValue("42", CliParameterType.NUMBER)).toBe(42);
|
||||||
);
|
// @ts-expect-error Testing private method
|
||||||
expect(
|
expect(cliCommand.castArgValue("true", CliParameterType.BOOLEAN)).toBe(
|
||||||
cliCommand["castArgValue"]("true", CliParameterType.BOOLEAN)
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
expect(
|
expect(
|
||||||
cliCommand["castArgValue"]("value1,value2", CliParameterType.ARRAY)
|
// @ts-expect-error Testing private method
|
||||||
).toEqual(["value1", "value2"]);
|
cliCommand.castArgValue("value1,value2", CliParameterType.ARRAY),
|
||||||
});
|
).toEqual(["value1", "value2"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should run the execute function with the parsed parameters", async () => {
|
it("should run the execute function with the parsed parameters", async () => {
|
||||||
const mockExecute = jest.fn();
|
const mockExecute = jest.fn();
|
||||||
cliCommand = new CliCommand(
|
cliCommand = new CliCommand(
|
||||||
["category1", "category2"],
|
["category1", "category2"],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "arg1",
|
name: "arg1",
|
||||||
type: CliParameterType.STRING,
|
type: CliParameterType.STRING,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg2",
|
name: "arg2",
|
||||||
type: CliParameterType.NUMBER,
|
type: CliParameterType.NUMBER,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg3",
|
name: "arg3",
|
||||||
type: CliParameterType.BOOLEAN,
|
type: CliParameterType.BOOLEAN,
|
||||||
needsValue: false,
|
needsValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg4",
|
name: "arg4",
|
||||||
type: CliParameterType.ARRAY,
|
type: CliParameterType.ARRAY,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
mockExecute
|
mockExecute,
|
||||||
);
|
);
|
||||||
|
|
||||||
await cliCommand.run([
|
await cliCommand.run([
|
||||||
"--arg1",
|
"--arg1",
|
||||||
"value1",
|
"value1",
|
||||||
"--arg2",
|
"--arg2",
|
||||||
"42",
|
"42",
|
||||||
"--arg3",
|
"--arg3",
|
||||||
"--arg4",
|
"--arg4",
|
||||||
"value1,value2",
|
"value1,value2",
|
||||||
]);
|
]);
|
||||||
expect(mockExecute).toHaveBeenCalledWith(cliCommand, {
|
expect(mockExecute).toHaveBeenCalledWith(cliCommand, {
|
||||||
arg1: "value1",
|
arg1: "value1",
|
||||||
arg2: 42,
|
arg2: 42,
|
||||||
arg3: true,
|
arg3: true,
|
||||||
arg4: ["value1", "value2"],
|
arg4: ["value1", "value2"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work with a mix of positioned and non-positioned arguments", async () => {
|
it("should work with a mix of positioned and non-positioned arguments", async () => {
|
||||||
const mockExecute = jest.fn();
|
const mockExecute = jest.fn();
|
||||||
cliCommand = new CliCommand(
|
cliCommand = new CliCommand(
|
||||||
["category1", "category2"],
|
["category1", "category2"],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "arg1",
|
name: "arg1",
|
||||||
type: CliParameterType.STRING,
|
type: CliParameterType.STRING,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg2",
|
name: "arg2",
|
||||||
type: CliParameterType.NUMBER,
|
type: CliParameterType.NUMBER,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg3",
|
name: "arg3",
|
||||||
type: CliParameterType.BOOLEAN,
|
type: CliParameterType.BOOLEAN,
|
||||||
needsValue: false,
|
needsValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg4",
|
name: "arg4",
|
||||||
type: CliParameterType.ARRAY,
|
type: CliParameterType.ARRAY,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg5",
|
name: "arg5",
|
||||||
type: CliParameterType.STRING,
|
type: CliParameterType.STRING,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
positioned: true,
|
positioned: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
mockExecute
|
mockExecute,
|
||||||
);
|
);
|
||||||
|
|
||||||
await cliCommand.run([
|
await cliCommand.run([
|
||||||
"--arg1",
|
"--arg1",
|
||||||
"value1",
|
"value1",
|
||||||
"--arg2",
|
"--arg2",
|
||||||
"42",
|
"42",
|
||||||
"--arg3",
|
"--arg3",
|
||||||
"--arg4",
|
"--arg4",
|
||||||
"value1,value2",
|
"value1,value2",
|
||||||
"value5",
|
"value5",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mockExecute).toHaveBeenCalledWith(cliCommand, {
|
expect(mockExecute).toHaveBeenCalledWith(cliCommand, {
|
||||||
arg1: "value1",
|
arg1: "value1",
|
||||||
arg2: 42,
|
arg2: 42,
|
||||||
arg3: true,
|
arg3: true,
|
||||||
arg4: ["value1", "value2"],
|
arg4: ["value1", "value2"],
|
||||||
arg5: "value5",
|
arg5: "value5",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display help message correctly", () => {
|
it("should display help message correctly", () => {
|
||||||
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {
|
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
});
|
});
|
||||||
|
|
||||||
cliCommand = new CliCommand(
|
cliCommand = new CliCommand(
|
||||||
["category1", "category2"],
|
["category1", "category2"],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "arg1",
|
name: "arg1",
|
||||||
type: CliParameterType.STRING,
|
type: CliParameterType.STRING,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
description: "Argument 1",
|
description: "Argument 1",
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg2",
|
name: "arg2",
|
||||||
type: CliParameterType.NUMBER,
|
type: CliParameterType.NUMBER,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
description: "Argument 2",
|
description: "Argument 2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg3",
|
name: "arg3",
|
||||||
type: CliParameterType.BOOLEAN,
|
type: CliParameterType.BOOLEAN,
|
||||||
needsValue: false,
|
needsValue: false,
|
||||||
description: "Argument 3",
|
description: "Argument 3",
|
||||||
optional: true,
|
optional: true,
|
||||||
positioned: false,
|
positioned: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg4",
|
name: "arg4",
|
||||||
type: CliParameterType.ARRAY,
|
type: CliParameterType.ARRAY,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
description: "Argument 4",
|
description: "Argument 4",
|
||||||
positioned: false,
|
positioned: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
},
|
},
|
||||||
"This is a test command",
|
"This is a test command",
|
||||||
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2"
|
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2",
|
||||||
);
|
);
|
||||||
|
|
||||||
cliCommand.displayHelp();
|
cliCommand.displayHelp();
|
||||||
|
|
||||||
const loggedString = consoleLogSpy.mock.calls.map(call =>
|
const loggedString = consoleLogSpy.mock.calls.map((call) =>
|
||||||
stripAnsi(call[0])
|
stripAnsi(call[0]),
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
consoleLogSpy.mockRestore();
|
consoleLogSpy.mockRestore();
|
||||||
|
|
||||||
expect(loggedString).toContain("📚 Command: category1 category2");
|
expect(loggedString).toContain("📚 Command: category1 category2");
|
||||||
expect(loggedString).toContain("🔧 Arguments:");
|
expect(loggedString).toContain("🔧 Arguments:");
|
||||||
expect(loggedString).toContain("arg1: Argument 1 (optional)");
|
expect(loggedString).toContain("arg1: Argument 1 (optional)");
|
||||||
expect(loggedString).toContain("arg2: Argument 2");
|
expect(loggedString).toContain("arg2: Argument 2");
|
||||||
expect(loggedString).toContain("--arg3: Argument 3 (optional)");
|
expect(loggedString).toContain("--arg3: Argument 3 (optional)");
|
||||||
expect(loggedString).toContain("--arg4: Argument 4");
|
expect(loggedString).toContain("--arg4: Argument 4");
|
||||||
expect(loggedString).toContain("🚀 Example:");
|
expect(loggedString).toContain("🚀 Example:");
|
||||||
expect(loggedString).toContain(
|
expect(loggedString).toContain(
|
||||||
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2"
|
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("CliBuilder", () => {
|
describe("CliBuilder", () => {
|
||||||
let cliBuilder: CliBuilder;
|
let cliBuilder: CliBuilder;
|
||||||
let mockCommand1: CliCommand;
|
let mockCommand1: CliCommand;
|
||||||
let mockCommand2: CliCommand;
|
let mockCommand2: CliCommand;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockCommand1 = new CliCommand(["category1"], [], jest.fn());
|
mockCommand1 = new CliCommand(["category1"], [], jest.fn());
|
||||||
mockCommand2 = new CliCommand(["category2"], [], jest.fn());
|
mockCommand2 = new CliCommand(["category2"], [], jest.fn());
|
||||||
cliBuilder = new CliBuilder([mockCommand1]);
|
cliBuilder = new CliBuilder([mockCommand1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register a command correctly", () => {
|
it("should register a command correctly", () => {
|
||||||
cliBuilder.registerCommand(mockCommand2);
|
cliBuilder.registerCommand(mockCommand2);
|
||||||
expect(cliBuilder.commands).toContain(mockCommand2);
|
expect(cliBuilder.commands).toContain(mockCommand2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register multiple commands correctly", () => {
|
it("should register multiple commands correctly", () => {
|
||||||
const mockCommand3 = new CliCommand(["category3"], [], jest.fn());
|
const mockCommand3 = new CliCommand(["category3"], [], jest.fn());
|
||||||
cliBuilder.registerCommands([mockCommand2, mockCommand3]);
|
cliBuilder.registerCommands([mockCommand2, mockCommand3]);
|
||||||
expect(cliBuilder.commands).toContain(mockCommand2);
|
expect(cliBuilder.commands).toContain(mockCommand2);
|
||||||
expect(cliBuilder.commands).toContain(mockCommand3);
|
expect(cliBuilder.commands).toContain(mockCommand3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should error when adding duplicates", () => {
|
it("should error when adding duplicates", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
cliBuilder.registerCommand(mockCommand1);
|
cliBuilder.registerCommand(mockCommand1);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
cliBuilder.registerCommands([mockCommand1]);
|
cliBuilder.registerCommands([mockCommand1]);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deregister a command correctly", () => {
|
it("should deregister a command correctly", () => {
|
||||||
cliBuilder.deregisterCommand(mockCommand1);
|
cliBuilder.deregisterCommand(mockCommand1);
|
||||||
expect(cliBuilder.commands).not.toContain(mockCommand1);
|
expect(cliBuilder.commands).not.toContain(mockCommand1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deregister multiple commands correctly", () => {
|
it("should deregister multiple commands correctly", () => {
|
||||||
cliBuilder.registerCommand(mockCommand2);
|
cliBuilder.registerCommand(mockCommand2);
|
||||||
cliBuilder.deregisterCommands([mockCommand1, mockCommand2]);
|
cliBuilder.deregisterCommands([mockCommand1, mockCommand2]);
|
||||||
expect(cliBuilder.commands).not.toContain(mockCommand1);
|
expect(cliBuilder.commands).not.toContain(mockCommand1);
|
||||||
expect(cliBuilder.commands).not.toContain(mockCommand2);
|
expect(cliBuilder.commands).not.toContain(mockCommand2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should process args correctly", async () => {
|
it("should process args correctly", async () => {
|
||||||
const mockExecute = jest.fn();
|
const mockExecute = jest.fn();
|
||||||
const mockCommand = new CliCommand(
|
const mockCommand = new CliCommand(
|
||||||
["category1", "sub1"],
|
["category1", "sub1"],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "arg1",
|
name: "arg1",
|
||||||
type: CliParameterType.STRING,
|
type: CliParameterType.STRING,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
positioned: false,
|
positioned: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
mockExecute
|
mockExecute,
|
||||||
);
|
);
|
||||||
cliBuilder.registerCommand(mockCommand);
|
cliBuilder.registerCommand(mockCommand);
|
||||||
await cliBuilder.processArgs([
|
await cliBuilder.processArgs([
|
||||||
"./cli.ts",
|
"./cli.ts",
|
||||||
"category1",
|
"category1",
|
||||||
"sub1",
|
"sub1",
|
||||||
"--arg1",
|
"--arg1",
|
||||||
"value1",
|
"value1",
|
||||||
]);
|
]);
|
||||||
expect(mockExecute).toHaveBeenCalledWith(expect.anything(), {
|
expect(mockExecute).toHaveBeenCalledWith(expect.anything(), {
|
||||||
arg1: "value1",
|
arg1: "value1",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should build command tree", () => {
|
describe("should build command tree", () => {
|
||||||
let cliBuilder: CliBuilder;
|
let cliBuilder: CliBuilder;
|
||||||
let mockCommand1: CliCommand;
|
let mockCommand1: CliCommand;
|
||||||
let mockCommand2: CliCommand;
|
let mockCommand2: CliCommand;
|
||||||
let mockCommand3: CliCommand;
|
let mockCommand3: CliCommand;
|
||||||
let mockCommand4: CliCommand;
|
let mockCommand4: CliCommand;
|
||||||
let mockCommand5: CliCommand;
|
let mockCommand5: CliCommand;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockCommand1 = new CliCommand(["user", "verify"], [], jest.fn());
|
mockCommand1 = new CliCommand(["user", "verify"], [], jest.fn());
|
||||||
mockCommand2 = new CliCommand(["user", "delete"], [], jest.fn());
|
mockCommand2 = new CliCommand(["user", "delete"], [], jest.fn());
|
||||||
mockCommand3 = new CliCommand(
|
mockCommand3 = new CliCommand(
|
||||||
["user", "new", "admin"],
|
["user", "new", "admin"],
|
||||||
[],
|
[],
|
||||||
jest.fn()
|
jest.fn(),
|
||||||
);
|
);
|
||||||
mockCommand4 = new CliCommand(["user", "new"], [], jest.fn());
|
mockCommand4 = new CliCommand(["user", "new"], [], jest.fn());
|
||||||
mockCommand5 = new CliCommand(["admin", "delete"], [], jest.fn());
|
mockCommand5 = new CliCommand(["admin", "delete"], [], jest.fn());
|
||||||
cliBuilder = new CliBuilder([
|
cliBuilder = new CliBuilder([
|
||||||
mockCommand1,
|
mockCommand1,
|
||||||
mockCommand2,
|
mockCommand2,
|
||||||
mockCommand3,
|
mockCommand3,
|
||||||
mockCommand4,
|
mockCommand4,
|
||||||
mockCommand5,
|
mockCommand5,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should build the command tree correctly", () => {
|
it("should build the command tree correctly", () => {
|
||||||
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
||||||
expect(tree).toEqual({
|
expect(tree).toEqual({
|
||||||
user: {
|
user: {
|
||||||
verify: mockCommand1,
|
verify: mockCommand1,
|
||||||
delete: mockCommand2,
|
delete: mockCommand2,
|
||||||
new: {
|
new: {
|
||||||
admin: mockCommand3,
|
admin: mockCommand3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
delete: mockCommand5,
|
delete: mockCommand5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should build the command tree correctly when there are no commands", () => {
|
it("should build the command tree correctly when there are no commands", () => {
|
||||||
cliBuilder = new CliBuilder([]);
|
cliBuilder = new CliBuilder([]);
|
||||||
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
||||||
expect(tree).toEqual({});
|
expect(tree).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should build the command tree correctly when there is only one command", () => {
|
it("should build the command tree correctly when there is only one command", () => {
|
||||||
cliBuilder = new CliBuilder([mockCommand1]);
|
cliBuilder = new CliBuilder([mockCommand1]);
|
||||||
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
||||||
expect(tree).toEqual({
|
expect(tree).toEqual({
|
||||||
user: {
|
user: {
|
||||||
verify: mockCommand1,
|
verify: mockCommand1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show help menu", () => {
|
it("should show help menu", () => {
|
||||||
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {
|
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
});
|
});
|
||||||
|
|
||||||
const cliBuilder = new CliBuilder();
|
const cliBuilder = new CliBuilder();
|
||||||
|
|
||||||
const cliCommand = new CliCommand(
|
const cliCommand = new CliCommand(
|
||||||
["category1", "category2"],
|
["category1", "category2"],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "name",
|
name: "name",
|
||||||
type: CliParameterType.STRING,
|
type: CliParameterType.STRING,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
description: "Name of new item",
|
description: "Name of new item",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "delete-previous",
|
name: "delete-previous",
|
||||||
type: CliParameterType.NUMBER,
|
type: CliParameterType.NUMBER,
|
||||||
needsValue: false,
|
needsValue: false,
|
||||||
positioned: false,
|
positioned: false,
|
||||||
optional: true,
|
optional: true,
|
||||||
description: "Also delete the previous item",
|
description: "Also delete the previous item",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg3",
|
name: "arg3",
|
||||||
type: CliParameterType.BOOLEAN,
|
type: CliParameterType.BOOLEAN,
|
||||||
needsValue: false,
|
needsValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arg4",
|
name: "arg4",
|
||||||
type: CliParameterType.ARRAY,
|
type: CliParameterType.ARRAY,
|
||||||
needsValue: true,
|
needsValue: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
},
|
},
|
||||||
"I love sussy sauces",
|
"I love sussy sauces",
|
||||||
"emoji add --url https://site.com/image.png"
|
"emoji add --url https://site.com/image.png",
|
||||||
);
|
);
|
||||||
|
|
||||||
cliBuilder.registerCommand(cliCommand);
|
cliBuilder.registerCommand(cliCommand);
|
||||||
cliBuilder.displayHelp();
|
cliBuilder.displayHelp();
|
||||||
|
|
||||||
const loggedString = consoleLogSpy.mock.calls
|
const loggedString = consoleLogSpy.mock.calls
|
||||||
.map(call => stripAnsi(call[0]))
|
.map((call) => stripAnsi(call[0]))
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
consoleLogSpy.mockRestore();
|
consoleLogSpy.mockRestore();
|
||||||
|
|
||||||
expect(loggedString).toContain("category1");
|
expect(loggedString).toContain("category1");
|
||||||
expect(loggedString).toContain(
|
expect(loggedString).toContain(
|
||||||
" category2.................I love sussy sauces"
|
" category2.................I love sussy sauces",
|
||||||
);
|
);
|
||||||
expect(loggedString).toContain(
|
expect(loggedString).toContain(
|
||||||
" name..................Name of new item"
|
" name..................Name of new item",
|
||||||
);
|
);
|
||||||
expect(loggedString).toContain(
|
expect(loggedString).toContain(
|
||||||
" arg3..................(no description)"
|
" arg3..................(no description)",
|
||||||
);
|
);
|
||||||
expect(loggedString).toContain(
|
expect(loggedString).toContain(
|
||||||
" arg4..................(no description)"
|
" arg4..................(no description)",
|
||||||
);
|
);
|
||||||
expect(loggedString).toContain(
|
expect(loggedString).toContain(
|
||||||
" --delete-previous.....Also delete the previous item (optional)"
|
" --delete-previous.....Also delete the previous item (optional)",
|
||||||
);
|
);
|
||||||
expect(loggedString).toContain(
|
expect(loggedString).toContain(
|
||||||
" Example: emoji add --url https://site.com/image.png"
|
" Example: emoji add --url https://site.com/image.png",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -6,18 +6,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { watchConfig } from "c12";
|
import { watchConfig } from "c12";
|
||||||
import { defaultConfig, type Config } from "./config.type";
|
import { type Config, defaultConfig } from "./config.type";
|
||||||
|
|
||||||
const { config } = await watchConfig<Config>({
|
const { config } = await watchConfig<Config>({
|
||||||
configFile: "./config/config.toml",
|
configFile: "./config/config.toml",
|
||||||
defaultConfig: defaultConfig,
|
defaultConfig: defaultConfig,
|
||||||
overrides:
|
overrides:
|
||||||
(
|
(
|
||||||
await watchConfig<Config>({
|
await watchConfig<Config>({
|
||||||
configFile: "./config/config.internal.toml",
|
configFile: "./config/config.internal.toml",
|
||||||
defaultConfig: {} as Config,
|
defaultConfig: {} as Config,
|
||||||
})
|
})
|
||||||
).config ?? undefined,
|
).config ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const exportedConfig = config ?? defaultConfig;
|
const exportedConfig = config ?? defaultConfig;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "config-manager",
|
"name": "config-manager",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": { "@iarna/toml": "^2.2.5", "merge-deep-ts": "^1.2.6" }
|
"dependencies": { "@iarna/toml": "^2.2.5", "merge-deep-ts": "^1.2.6" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
|
import { appendFile } from "node:fs/promises";
|
||||||
import type { BunFile } from "bun";
|
import type { BunFile } from "bun";
|
||||||
import { appendFile } from "fs/promises";
|
|
||||||
|
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
DEBUG = "debug",
|
DEBUG = "debug",
|
||||||
INFO = "info",
|
INFO = "info",
|
||||||
WARNING = "warning",
|
WARNING = "warning",
|
||||||
ERROR = "error",
|
ERROR = "error",
|
||||||
CRITICAL = "critical",
|
CRITICAL = "critical",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -14,161 +14,165 @@ export enum LogLevel {
|
||||||
* @param output BunFile of output (can be a normal file or something like Bun.stdout)
|
* @param output BunFile of output (can be a normal file or something like Bun.stdout)
|
||||||
*/
|
*/
|
||||||
export class LogManager {
|
export class LogManager {
|
||||||
constructor(private output: BunFile) {
|
constructor(private output: BunFile) {
|
||||||
void this.write(
|
void this.write(
|
||||||
`--- INIT LogManager at ${new Date().toISOString()} ---`
|
`--- INIT LogManager at ${new Date().toISOString()} ---`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a message to the output
|
* Logs a message to the output
|
||||||
* @param level Importance of the log
|
* @param level Importance of the log
|
||||||
* @param entity Emitter of the log
|
* @param entity Emitter of the log
|
||||||
* @param message Message to log
|
* @param message Message to log
|
||||||
* @param showTimestamp Whether to show the timestamp in the log
|
* @param showTimestamp Whether to show the timestamp in the log
|
||||||
*/
|
*/
|
||||||
async log(
|
async log(
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
entity: string,
|
entity: string,
|
||||||
message: string,
|
message: string,
|
||||||
showTimestamp = true
|
showTimestamp = true,
|
||||||
) {
|
) {
|
||||||
await this.write(
|
await this.write(
|
||||||
`${showTimestamp ? new Date().toISOString() + " " : ""}[${level.toUpperCase()}] ${entity}: ${message}`
|
`${
|
||||||
);
|
showTimestamp ? `${new Date().toISOString()} ` : ""
|
||||||
}
|
}[${level.toUpperCase()}] ${entity}: ${message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async write(text: string) {
|
private async write(text: string) {
|
||||||
if (this.output == Bun.stdout) {
|
if (this.output === Bun.stdout) {
|
||||||
await Bun.write(Bun.stdout, text + "\n");
|
await Bun.write(Bun.stdout, `${text}\n`);
|
||||||
} else {
|
} else {
|
||||||
if (!(await this.output.exists())) {
|
if (!(await this.output.exists())) {
|
||||||
// Create file if it doesn't exist
|
// Create file if it doesn't exist
|
||||||
await Bun.write(this.output, "", {
|
await Bun.write(this.output, "", {
|
||||||
createPath: true,
|
createPath: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await appendFile(this.output.name ?? "", text + "\n");
|
await appendFile(this.output.name ?? "", `${text}\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs an error to the output, wrapper for log
|
* Logs an error to the output, wrapper for log
|
||||||
* @param level Importance of the log
|
* @param level Importance of the log
|
||||||
* @param entity Emitter of the log
|
* @param entity Emitter of the log
|
||||||
* @param error Error to log
|
* @param error Error to log
|
||||||
*/
|
*/
|
||||||
async logError(level: LogLevel, entity: string, error: Error) {
|
async logError(level: LogLevel, entity: string, error: Error) {
|
||||||
await this.log(level, entity, error.message);
|
await this.log(level, entity, error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a request to the output
|
* Logs a request to the output
|
||||||
* @param req Request to log
|
* @param req Request to log
|
||||||
* @param ip IP of the request
|
* @param ip IP of the request
|
||||||
* @param logAllDetails Whether to log all details of the request
|
* @param logAllDetails Whether to log all details of the request
|
||||||
*/
|
*/
|
||||||
async logRequest(req: Request, ip?: string, logAllDetails = false) {
|
async logRequest(req: Request, ip?: string, logAllDetails = false) {
|
||||||
let string = ip ? `${ip}: ` : "";
|
let string = ip ? `${ip}: ` : "";
|
||||||
|
|
||||||
string += `${req.method} ${req.url}`;
|
string += `${req.method} ${req.url}`;
|
||||||
|
|
||||||
if (logAllDetails) {
|
if (logAllDetails) {
|
||||||
string += `\n`;
|
string += "\n";
|
||||||
string += ` [Headers]\n`;
|
string += " [Headers]\n";
|
||||||
// Pretty print headers
|
// Pretty print headers
|
||||||
for (const [key, value] of req.headers.entries()) {
|
for (const [key, value] of req.headers.entries()) {
|
||||||
string += ` ${key}: ${value}\n`;
|
string += ` ${key}: ${value}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pretty print body
|
// Pretty print body
|
||||||
string += ` [Body]\n`;
|
string += " [Body]\n";
|
||||||
const content_type = req.headers.get("Content-Type");
|
const content_type = req.headers.get("Content-Type");
|
||||||
|
|
||||||
if (content_type && content_type.includes("application/json")) {
|
if (content_type?.includes("application/json")) {
|
||||||
const json = await req.json();
|
const json = await req.json();
|
||||||
const stringified = JSON.stringify(json, null, 4)
|
const stringified = JSON.stringify(json, null, 4)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(line => ` ${line}`)
|
.map((line) => ` ${line}`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
string += `${stringified}\n`;
|
string += `${stringified}\n`;
|
||||||
} else if (
|
} else if (
|
||||||
content_type &&
|
content_type &&
|
||||||
(content_type.includes("application/x-www-form-urlencoded") ||
|
(content_type.includes("application/x-www-form-urlencoded") ||
|
||||||
content_type.includes("multipart/form-data"))
|
content_type.includes("multipart/form-data"))
|
||||||
) {
|
) {
|
||||||
const formData = await req.formData();
|
const formData = await req.formData();
|
||||||
for (const [key, value] of formData.entries()) {
|
for (const [key, value] of formData.entries()) {
|
||||||
if (value.toString().length < 300) {
|
if (value.toString().length < 300) {
|
||||||
string += ` ${key}: ${value.toString()}\n`;
|
string += ` ${key}: ${value.toString()}\n`;
|
||||||
} else {
|
} else {
|
||||||
string += ` ${key}: <${value.toString().length} bytes>\n`;
|
string += ` ${key}: <${
|
||||||
}
|
value.toString().length
|
||||||
}
|
} bytes>\n`;
|
||||||
} else {
|
}
|
||||||
const text = await req.text();
|
}
|
||||||
string += ` ${text}\n`;
|
} else {
|
||||||
}
|
const text = await req.text();
|
||||||
}
|
string += ` ${text}\n`;
|
||||||
await this.log(LogLevel.INFO, "Request", string);
|
}
|
||||||
}
|
}
|
||||||
|
await this.log(LogLevel.INFO, "Request", string);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outputs to multiple LogManager instances at once
|
* Outputs to multiple LogManager instances at once
|
||||||
*/
|
*/
|
||||||
export class MultiLogManager {
|
export class MultiLogManager {
|
||||||
constructor(private logManagers: LogManager[]) {}
|
constructor(private logManagers: LogManager[]) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a message to all logManagers
|
* Logs a message to all logManagers
|
||||||
* @param level Importance of the log
|
* @param level Importance of the log
|
||||||
* @param entity Emitter of the log
|
* @param entity Emitter of the log
|
||||||
* @param message Message to log
|
* @param message Message to log
|
||||||
* @param showTimestamp Whether to show the timestamp in the log
|
* @param showTimestamp Whether to show the timestamp in the log
|
||||||
*/
|
*/
|
||||||
async log(
|
async log(
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
entity: string,
|
entity: string,
|
||||||
message: string,
|
message: string,
|
||||||
showTimestamp = true
|
showTimestamp = true,
|
||||||
) {
|
) {
|
||||||
for (const logManager of this.logManagers) {
|
for (const logManager of this.logManagers) {
|
||||||
await logManager.log(level, entity, message, showTimestamp);
|
await logManager.log(level, entity, message, showTimestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs an error to all logManagers
|
* Logs an error to all logManagers
|
||||||
* @param level Importance of the log
|
* @param level Importance of the log
|
||||||
* @param entity Emitter of the log
|
* @param entity Emitter of the log
|
||||||
* @param error Error to log
|
* @param error Error to log
|
||||||
*/
|
*/
|
||||||
async logError(level: LogLevel, entity: string, error: Error) {
|
async logError(level: LogLevel, entity: string, error: Error) {
|
||||||
for (const logManager of this.logManagers) {
|
for (const logManager of this.logManagers) {
|
||||||
await logManager.logError(level, entity, error);
|
await logManager.logError(level, entity, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a request to all logManagers
|
* Logs a request to all logManagers
|
||||||
* @param req Request to log
|
* @param req Request to log
|
||||||
* @param ip IP of the request
|
* @param ip IP of the request
|
||||||
* @param logAllDetails Whether to log all details of the request
|
* @param logAllDetails Whether to log all details of the request
|
||||||
*/
|
*/
|
||||||
async logRequest(req: Request, ip?: string, logAllDetails = false) {
|
async logRequest(req: Request, ip?: string, logAllDetails = false) {
|
||||||
for (const logManager of this.logManagers) {
|
for (const logManager of this.logManagers) {
|
||||||
await logManager.logRequest(req, ip, logAllDetails);
|
await logManager.logRequest(req, ip, logAllDetails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a MultiLogManager from multiple LogManager instances
|
* Create a MultiLogManager from multiple LogManager instances
|
||||||
* @param logManagers LogManager instances to use
|
* @param logManagers LogManager instances to use
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static fromLogManagers(...logManagers: LogManager[]) {
|
static fromLogManagers(...logManagers: LogManager[]) {
|
||||||
return new MultiLogManager(logManagers);
|
return new MultiLogManager(logManagers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
"name": "log-manager",
|
"name": "log-manager",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": { }
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,117 +1,117 @@
|
||||||
// FILEPATH: /home/jessew/Dev/lysand/packages/log-manager/log-manager.test.ts
|
|
||||||
import { LogManager, LogLevel, MultiLogManager } from "../index";
|
|
||||||
import type fs from "fs/promises";
|
|
||||||
import {
|
import {
|
||||||
describe,
|
type Mock,
|
||||||
it,
|
beforeEach,
|
||||||
beforeEach,
|
describe,
|
||||||
expect,
|
expect,
|
||||||
jest,
|
it,
|
||||||
mock,
|
jest,
|
||||||
type Mock,
|
mock,
|
||||||
test,
|
test,
|
||||||
} from "bun:test";
|
} from "bun:test";
|
||||||
|
import type fs from "node:fs/promises";
|
||||||
import type { BunFile } from "bun";
|
import type { BunFile } from "bun";
|
||||||
|
// FILEPATH: /home/jessew/Dev/lysand/packages/log-manager/log-manager.test.ts
|
||||||
|
import { LogLevel, LogManager, MultiLogManager } from "../index";
|
||||||
|
|
||||||
describe("LogManager", () => {
|
describe("LogManager", () => {
|
||||||
let logManager: LogManager;
|
let logManager: LogManager;
|
||||||
let mockOutput: BunFile;
|
let mockOutput: BunFile;
|
||||||
let mockAppend: Mock<typeof fs.appendFile>;
|
let mockAppend: Mock<typeof fs.appendFile>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockOutput = Bun.file("test.log");
|
mockOutput = Bun.file("test.log");
|
||||||
mockAppend = jest.fn();
|
mockAppend = jest.fn();
|
||||||
await mock.module("fs/promises", () => ({
|
await mock.module("fs/promises", () => ({
|
||||||
appendFile: mockAppend,
|
appendFile: mockAppend,
|
||||||
}));
|
}));
|
||||||
logManager = new LogManager(mockOutput);
|
logManager = new LogManager(mockOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize and write init log", () => {
|
it("should initialize and write init log", () => {
|
||||||
expect(mockAppend).toHaveBeenCalledWith(
|
expect(mockAppend).toHaveBeenCalledWith(
|
||||||
mockOutput.name,
|
mockOutput.name,
|
||||||
expect.stringContaining("--- INIT LogManager at")
|
expect.stringContaining("--- INIT LogManager at"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log message with timestamp", async () => {
|
it("should log message with timestamp", async () => {
|
||||||
await logManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
await logManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
||||||
expect(mockAppend).toHaveBeenCalledWith(
|
expect(mockAppend).toHaveBeenCalledWith(
|
||||||
mockOutput.name,
|
mockOutput.name,
|
||||||
expect.stringContaining("[INFO] TestEntity: Test message")
|
expect.stringContaining("[INFO] TestEntity: Test message"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log message without timestamp", async () => {
|
it("should log message without timestamp", async () => {
|
||||||
await logManager.log(
|
await logManager.log(
|
||||||
LogLevel.INFO,
|
LogLevel.INFO,
|
||||||
"TestEntity",
|
"TestEntity",
|
||||||
"Test message",
|
"Test message",
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
expect(mockAppend).toHaveBeenCalledWith(
|
expect(mockAppend).toHaveBeenCalledWith(
|
||||||
mockOutput.name,
|
mockOutput.name,
|
||||||
"[INFO] TestEntity: Test message\n"
|
"[INFO] TestEntity: Test message\n",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("should write to stdout", async () => {
|
test.skip("should write to stdout", async () => {
|
||||||
logManager = new LogManager(Bun.stdout);
|
logManager = new LogManager(Bun.stdout);
|
||||||
await logManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
await logManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
||||||
|
|
||||||
const writeMock = jest.fn();
|
const writeMock = jest.fn();
|
||||||
|
|
||||||
await mock.module("Bun", () => ({
|
await mock.module("Bun", () => ({
|
||||||
stdout: Bun.stdout,
|
stdout: Bun.stdout,
|
||||||
write: writeMock,
|
write: writeMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
expect(writeMock).toHaveBeenCalledWith(
|
expect(writeMock).toHaveBeenCalledWith(
|
||||||
Bun.stdout,
|
Bun.stdout,
|
||||||
expect.stringContaining("[INFO] TestEntity: Test message")
|
expect.stringContaining("[INFO] TestEntity: Test message"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error if output file does not exist", () => {
|
it("should throw error if output file does not exist", () => {
|
||||||
mockAppend.mockImplementationOnce(() => {
|
mockAppend.mockImplementationOnce(() => {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error("Output file doesnt exist (and isnt stdout)")
|
new Error("Output file doesnt exist (and isnt stdout)"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
logManager.log(LogLevel.INFO, "TestEntity", "Test message")
|
logManager.log(LogLevel.INFO, "TestEntity", "Test message"),
|
||||||
).rejects.toThrow(Error);
|
).rejects.toThrow(Error);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log error message", async () => {
|
it("should log error message", async () => {
|
||||||
const error = new Error("Test error");
|
const error = new Error("Test error");
|
||||||
await logManager.logError(LogLevel.ERROR, "TestEntity", error);
|
await logManager.logError(LogLevel.ERROR, "TestEntity", error);
|
||||||
expect(mockAppend).toHaveBeenCalledWith(
|
expect(mockAppend).toHaveBeenCalledWith(
|
||||||
mockOutput.name,
|
mockOutput.name,
|
||||||
expect.stringContaining("[ERROR] TestEntity: Test error")
|
expect.stringContaining("[ERROR] TestEntity: Test error"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log basic request details", async () => {
|
it("should log basic request details", async () => {
|
||||||
const req = new Request("http://localhost/test", { method: "GET" });
|
const req = new Request("http://localhost/test", { method: "GET" });
|
||||||
await logManager.logRequest(req, "127.0.0.1");
|
await logManager.logRequest(req, "127.0.0.1");
|
||||||
|
|
||||||
expect(mockAppend).toHaveBeenCalledWith(
|
expect(mockAppend).toHaveBeenCalledWith(
|
||||||
mockOutput.name,
|
mockOutput.name,
|
||||||
expect.stringContaining("127.0.0.1: GET http://localhost/test")
|
expect.stringContaining("127.0.0.1: GET http://localhost/test"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Request logger", () => {
|
describe("Request logger", () => {
|
||||||
it("should log all request details for JSON content type", async () => {
|
it("should log all request details for JSON content type", async () => {
|
||||||
const req = new Request("http://localhost/test", {
|
const req = new Request("http://localhost/test", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ test: "value" }),
|
body: JSON.stringify({ test: "value" }),
|
||||||
});
|
});
|
||||||
await logManager.logRequest(req, "127.0.0.1", true);
|
await logManager.logRequest(req, "127.0.0.1", true);
|
||||||
|
|
||||||
const expectedLog = `127.0.0.1: POST http://localhost/test
|
const expectedLog = `127.0.0.1: POST http://localhost/test
|
||||||
[Headers]
|
[Headers]
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
[Body]
|
[Body]
|
||||||
|
|
@ -120,112 +120,112 @@ describe("LogManager", () => {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
expect(mockAppend).toHaveBeenCalledWith(
|
expect(mockAppend).toHaveBeenCalledWith(
|
||||||
mockOutput.name,
|
mockOutput.name,
|
||||||
expect.stringContaining(expectedLog)
|
expect.stringContaining(expectedLog),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log all request details for text content type", async () => {
|
it("should log all request details for text content type", async () => {
|
||||||
const req = new Request("http://localhost/test", {
|
const req = new Request("http://localhost/test", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "text/plain" },
|
headers: { "Content-Type": "text/plain" },
|
||||||
body: "Test body",
|
body: "Test body",
|
||||||
});
|
});
|
||||||
await logManager.logRequest(req, "127.0.0.1", true);
|
await logManager.logRequest(req, "127.0.0.1", true);
|
||||||
|
|
||||||
const expectedLog = `127.0.0.1: POST http://localhost/test
|
const expectedLog = `127.0.0.1: POST http://localhost/test
|
||||||
[Headers]
|
[Headers]
|
||||||
content-type: text/plain
|
content-type: text/plain
|
||||||
[Body]
|
[Body]
|
||||||
Test body
|
Test body
|
||||||
`;
|
`;
|
||||||
expect(mockAppend).toHaveBeenCalledWith(
|
expect(mockAppend).toHaveBeenCalledWith(
|
||||||
mockOutput.name,
|
mockOutput.name,
|
||||||
expect.stringContaining(expectedLog)
|
expect.stringContaining(expectedLog),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log all request details for FormData content-type", async () => {
|
it("should log all request details for FormData content-type", async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("test", "value");
|
formData.append("test", "value");
|
||||||
const req = new Request("http://localhost/test", {
|
const req = new Request("http://localhost/test", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
await logManager.logRequest(req, "127.0.0.1", true);
|
await logManager.logRequest(req, "127.0.0.1", true);
|
||||||
|
|
||||||
const expectedLog = `127.0.0.1: POST http://localhost/test
|
const expectedLog = `127.0.0.1: POST http://localhost/test
|
||||||
[Headers]
|
[Headers]
|
||||||
content-type: multipart/form-data; boundary=${
|
content-type: multipart/form-data; boundary=${
|
||||||
req.headers.get("Content-Type")?.split("boundary=")[1] ?? ""
|
req.headers.get("Content-Type")?.split("boundary=")[1] ?? ""
|
||||||
}
|
}
|
||||||
[Body]
|
[Body]
|
||||||
test: value
|
test: value
|
||||||
`;
|
`;
|
||||||
|
|
||||||
expect(mockAppend).toHaveBeenCalledWith(
|
expect(mockAppend).toHaveBeenCalledWith(
|
||||||
mockOutput.name,
|
mockOutput.name,
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
expectedLog.replace("----", expect.any(String))
|
expectedLog.replace("----", expect.any(String)),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("MultiLogManager", () => {
|
describe("MultiLogManager", () => {
|
||||||
let multiLogManager: MultiLogManager;
|
let multiLogManager: MultiLogManager;
|
||||||
let mockLogManagers: LogManager[];
|
let mockLogManagers: LogManager[];
|
||||||
let mockLog: jest.Mock;
|
let mockLog: jest.Mock;
|
||||||
let mockLogError: jest.Mock;
|
let mockLogError: jest.Mock;
|
||||||
let mockLogRequest: jest.Mock;
|
let mockLogRequest: jest.Mock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLog = jest.fn();
|
mockLog = jest.fn();
|
||||||
mockLogError = jest.fn();
|
mockLogError = jest.fn();
|
||||||
mockLogRequest = jest.fn();
|
mockLogRequest = jest.fn();
|
||||||
mockLogManagers = [
|
mockLogManagers = [
|
||||||
{
|
{
|
||||||
log: mockLog,
|
log: mockLog,
|
||||||
logError: mockLogError,
|
logError: mockLogError,
|
||||||
logRequest: mockLogRequest,
|
logRequest: mockLogRequest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
log: mockLog,
|
log: mockLog,
|
||||||
logError: mockLogError,
|
logError: mockLogError,
|
||||||
logRequest: mockLogRequest,
|
logRequest: mockLogRequest,
|
||||||
},
|
},
|
||||||
] as unknown as LogManager[];
|
] as unknown as LogManager[];
|
||||||
multiLogManager = MultiLogManager.fromLogManagers(...mockLogManagers);
|
multiLogManager = MultiLogManager.fromLogManagers(...mockLogManagers);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log message to all logManagers", async () => {
|
it("should log message to all logManagers", async () => {
|
||||||
await multiLogManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
await multiLogManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
||||||
expect(mockLog).toHaveBeenCalledTimes(2);
|
expect(mockLog).toHaveBeenCalledTimes(2);
|
||||||
expect(mockLog).toHaveBeenCalledWith(
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
LogLevel.INFO,
|
LogLevel.INFO,
|
||||||
"TestEntity",
|
"TestEntity",
|
||||||
"Test message",
|
"Test message",
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log error to all logManagers", async () => {
|
it("should log error to all logManagers", async () => {
|
||||||
const error = new Error("Test error");
|
const error = new Error("Test error");
|
||||||
await multiLogManager.logError(LogLevel.ERROR, "TestEntity", error);
|
await multiLogManager.logError(LogLevel.ERROR, "TestEntity", error);
|
||||||
expect(mockLogError).toHaveBeenCalledTimes(2);
|
expect(mockLogError).toHaveBeenCalledTimes(2);
|
||||||
expect(mockLogError).toHaveBeenCalledWith(
|
expect(mockLogError).toHaveBeenCalledWith(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"TestEntity",
|
"TestEntity",
|
||||||
error
|
error,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log request to all logManagers", async () => {
|
it("should log request to all logManagers", async () => {
|
||||||
const req = new Request("http://localhost/test", { method: "GET" });
|
const req = new Request("http://localhost/test", { method: "GET" });
|
||||||
await multiLogManager.logRequest(req, "127.0.0.1", true);
|
await multiLogManager.logRequest(req, "127.0.0.1", true);
|
||||||
expect(mockLogRequest).toHaveBeenCalledTimes(2);
|
expect(mockLogRequest).toHaveBeenCalledTimes(2);
|
||||||
expect(mockLogRequest).toHaveBeenCalledWith(req, "127.0.0.1", true);
|
expect(mockLogRequest).toHaveBeenCalledWith(req, "127.0.0.1", true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,65 @@
|
||||||
|
import type { Config } from "config-manager";
|
||||||
|
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||||
import type { ConvertableMediaFormats } from "../media-converter";
|
import type { ConvertableMediaFormats } from "../media-converter";
|
||||||
import { MediaConverter } from "../media-converter";
|
import { MediaConverter } from "../media-converter";
|
||||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
|
||||||
import type { ConfigType } from "config-manager";
|
|
||||||
|
|
||||||
export class LocalMediaBackend extends MediaBackend {
|
export class LocalMediaBackend extends MediaBackend {
|
||||||
constructor(config: ConfigType) {
|
constructor(config: Config) {
|
||||||
super(config, MediaBackendType.LOCAL);
|
super(config, MediaBackendType.LOCAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addFile(file: File) {
|
public async addFile(file: File) {
|
||||||
if (this.shouldConvertImages(this.config)) {
|
let convertedFile = file;
|
||||||
const fileExtension = file.name.split(".").pop();
|
if (this.shouldConvertImages(this.config)) {
|
||||||
const mediaConverter = new MediaConverter(
|
const fileExtension = file.name.split(".").pop();
|
||||||
fileExtension as ConvertableMediaFormats,
|
const mediaConverter = new MediaConverter(
|
||||||
this.config.media.conversion
|
fileExtension as ConvertableMediaFormats,
|
||||||
.convert_to as ConvertableMediaFormats
|
this.config.media.conversion
|
||||||
);
|
.convert_to as ConvertableMediaFormats,
|
||||||
file = await mediaConverter.convert(file);
|
);
|
||||||
}
|
convertedFile = await mediaConverter.convert(file);
|
||||||
|
}
|
||||||
|
|
||||||
const hash = await new MediaHasher().getMediaHash(file);
|
const hash = await new MediaHasher().getMediaHash(convertedFile);
|
||||||
|
|
||||||
const newFile = Bun.file(
|
const newFile = Bun.file(
|
||||||
`${this.config.media.local_uploads_folder}/${hash}`
|
`${this.config.media.local_uploads_folder}/${hash}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (await newFile.exists()) {
|
if (await newFile.exists()) {
|
||||||
throw new Error("File already exists");
|
throw new Error("File already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
await Bun.write(newFile, file);
|
await Bun.write(newFile, convertedFile);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uploadedFile: file,
|
uploadedFile: convertedFile,
|
||||||
path: `./uploads/${file.name}`,
|
path: `./uploads/${convertedFile.name}`,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFileByHash(
|
public async getFileByHash(
|
||||||
hash: string,
|
hash: string,
|
||||||
databaseHashFetcher: (sha256: string) => Promise<string | null>
|
databaseHashFetcher: (sha256: string) => Promise<string | null>,
|
||||||
): Promise<File | null> {
|
): Promise<File | null> {
|
||||||
const filename = await databaseHashFetcher(hash);
|
const filename = await databaseHashFetcher(hash);
|
||||||
|
|
||||||
if (!filename) return null;
|
if (!filename) return null;
|
||||||
|
|
||||||
return this.getFile(filename);
|
return this.getFile(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFile(filename: string): Promise<File | null> {
|
public async getFile(filename: string): Promise<File | null> {
|
||||||
const file = Bun.file(
|
const file = Bun.file(
|
||||||
`${this.config.media.local_uploads_folder}/${filename}`
|
`${this.config.media.local_uploads_folder}/${filename}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!(await file.exists())) return null;
|
if (!(await file.exists())) return null;
|
||||||
|
|
||||||
return new File([await file.arrayBuffer()], filename, {
|
return new File([await file.arrayBuffer()], filename, {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
lastModified: file.lastModified,
|
lastModified: file.lastModified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,74 @@
|
||||||
import { S3Client } from "@jsr/bradenmacdonald__s3-lite-client";
|
import { S3Client } from "@jsr/bradenmacdonald__s3-lite-client";
|
||||||
|
import type { Config } from "config-manager";
|
||||||
|
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||||
import type { ConvertableMediaFormats } from "../media-converter";
|
import type { ConvertableMediaFormats } from "../media-converter";
|
||||||
import { MediaConverter } from "../media-converter";
|
import { MediaConverter } from "../media-converter";
|
||||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
|
||||||
import type { ConfigType } from "config-manager";
|
|
||||||
|
|
||||||
export class S3MediaBackend extends MediaBackend {
|
export class S3MediaBackend extends MediaBackend {
|
||||||
constructor(
|
constructor(
|
||||||
config: ConfigType,
|
config: Config,
|
||||||
private s3Client = new S3Client({
|
private s3Client = new S3Client({
|
||||||
endPoint: config.s3.endpoint,
|
endPoint: config.s3.endpoint,
|
||||||
useSSL: true,
|
useSSL: true,
|
||||||
region: config.s3.region || "auto",
|
region: config.s3.region || "auto",
|
||||||
bucket: config.s3.bucket_name,
|
bucket: config.s3.bucket_name,
|
||||||
accessKey: config.s3.access_key,
|
accessKey: config.s3.access_key,
|
||||||
secretKey: config.s3.secret_access_key,
|
secretKey: config.s3.secret_access_key,
|
||||||
})
|
}),
|
||||||
) {
|
) {
|
||||||
super(config, MediaBackendType.S3);
|
super(config, MediaBackendType.S3);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addFile(file: File) {
|
public async addFile(file: File) {
|
||||||
if (this.shouldConvertImages(this.config)) {
|
let convertedFile = file;
|
||||||
const fileExtension = file.name.split(".").pop();
|
if (this.shouldConvertImages(this.config)) {
|
||||||
const mediaConverter = new MediaConverter(
|
const fileExtension = file.name.split(".").pop();
|
||||||
fileExtension as ConvertableMediaFormats,
|
const mediaConverter = new MediaConverter(
|
||||||
this.config.media.conversion
|
fileExtension as ConvertableMediaFormats,
|
||||||
.convert_to as ConvertableMediaFormats
|
this.config.media.conversion
|
||||||
);
|
.convert_to as ConvertableMediaFormats,
|
||||||
file = await mediaConverter.convert(file);
|
);
|
||||||
}
|
convertedFile = await mediaConverter.convert(file);
|
||||||
|
}
|
||||||
|
|
||||||
const hash = await new MediaHasher().getMediaHash(file);
|
const hash = await new MediaHasher().getMediaHash(convertedFile);
|
||||||
|
|
||||||
await this.s3Client.putObject(file.name, file.stream(), {
|
await this.s3Client.putObject(
|
||||||
size: file.size,
|
convertedFile.name,
|
||||||
});
|
convertedFile.stream(),
|
||||||
|
{
|
||||||
|
size: convertedFile.size,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uploadedFile: file,
|
uploadedFile: convertedFile,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFileByHash(
|
public async getFileByHash(
|
||||||
hash: string,
|
hash: string,
|
||||||
databaseHashFetcher: (sha256: string) => Promise<string | null>
|
databaseHashFetcher: (sha256: string) => Promise<string | null>,
|
||||||
): Promise<File | null> {
|
): Promise<File | null> {
|
||||||
const filename = await databaseHashFetcher(hash);
|
const filename = await databaseHashFetcher(hash);
|
||||||
|
|
||||||
if (!filename) return null;
|
if (!filename) return null;
|
||||||
|
|
||||||
return this.getFile(filename);
|
return this.getFile(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFile(filename: string): Promise<File | null> {
|
public async getFile(filename: string): Promise<File | null> {
|
||||||
try {
|
try {
|
||||||
await this.s3Client.statObject(filename);
|
await this.s3Client.statObject(filename);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await this.s3Client.getObject(filename);
|
const file = await this.s3Client.getObject(filename);
|
||||||
|
|
||||||
return new File([await file.arrayBuffer()], filename, {
|
return new File([await file.arrayBuffer()], filename, {
|
||||||
type: file.headers.get("Content-Type") || "undefined",
|
type: file.headers.get("Content-Type") || "undefined",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,101 @@
|
||||||
import type { ConfigType } from "config-manager";
|
import type { Config } from "config-manager";
|
||||||
|
|
||||||
export enum MediaBackendType {
|
export enum MediaBackendType {
|
||||||
LOCAL = "local",
|
LOCAL = "local",
|
||||||
S3 = "s3",
|
S3 = "s3",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadedFileMetadata {
|
interface UploadedFileMetadata {
|
||||||
uploadedFile: File;
|
uploadedFile: File;
|
||||||
path?: string;
|
path?: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MediaHasher {
|
export class MediaHasher {
|
||||||
/**
|
/**
|
||||||
* Returns the SHA-256 hash of a file in hex format
|
* Returns the SHA-256 hash of a file in hex format
|
||||||
* @param media The file to hash
|
* @param media The file to hash
|
||||||
* @returns The SHA-256 hash of the file in hex format
|
* @returns The SHA-256 hash of the file in hex format
|
||||||
*/
|
*/
|
||||||
public async getMediaHash(media: File) {
|
public async getMediaHash(media: File) {
|
||||||
const hash = new Bun.SHA256()
|
const hash = new Bun.SHA256()
|
||||||
.update(await media.arrayBuffer())
|
.update(await media.arrayBuffer())
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MediaBackend {
|
export class MediaBackend {
|
||||||
constructor(
|
constructor(
|
||||||
public config: ConfigType,
|
public config: Config,
|
||||||
public backend: MediaBackendType
|
public backend: MediaBackendType,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static async fromBackendType(
|
static async fromBackendType(
|
||||||
backend: MediaBackendType,
|
backend: MediaBackendType,
|
||||||
config: ConfigType
|
config: Config,
|
||||||
): Promise<MediaBackend> {
|
): Promise<MediaBackend> {
|
||||||
switch (backend) {
|
switch (backend) {
|
||||||
case MediaBackendType.LOCAL:
|
case MediaBackendType.LOCAL:
|
||||||
return new (await import("./backends/local")).LocalMediaBackend(
|
return new (await import("./backends/local")).LocalMediaBackend(
|
||||||
config
|
config,
|
||||||
);
|
);
|
||||||
case MediaBackendType.S3:
|
case MediaBackendType.S3:
|
||||||
return new (await import("./backends/s3")).S3MediaBackend(
|
return new (await import("./backends/s3")).S3MediaBackend(
|
||||||
config
|
config,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown backend type: ${backend as any}`);
|
throw new Error(`Unknown backend type: ${backend as string}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBackendType() {
|
public getBackendType() {
|
||||||
return this.backend;
|
return this.backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldConvertImages(config: ConfigType) {
|
public shouldConvertImages(config: Config) {
|
||||||
return config.media.conversion.convert_images;
|
return config.media.conversion.convert_images;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches file from backend from SHA-256 hash
|
* Fetches file from backend from SHA-256 hash
|
||||||
* @param file SHA-256 hash of wanted file
|
* @param file SHA-256 hash of wanted file
|
||||||
* @param databaseHashFetcher Function that takes in a sha256 hash as input and outputs the filename of that file in the database
|
* @param databaseHashFetcher Function that takes in a sha256 hash as input and outputs the filename of that file in the database
|
||||||
* @returns The file as a File object
|
* @returns The file as a File object
|
||||||
*/
|
*/
|
||||||
public getFileByHash(
|
public getFileByHash(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
file: string,
|
file: string,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
databaseHashFetcher: (sha256: string) => Promise<string>
|
databaseHashFetcher: (sha256: string) => Promise<string>,
|
||||||
): Promise<File | null> {
|
): Promise<File | null> {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error("Do not call MediaBackend directly: use a subclass")
|
new Error("Do not call MediaBackend directly: use a subclass"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches file from backend from filename
|
* Fetches file from backend from filename
|
||||||
* @param filename File name
|
* @param filename File name
|
||||||
* @returns The file as a File object
|
* @returns The file as a File object
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public getFile(filename: string): Promise<File | null> {
|
public getFile(filename: string): Promise<File | null> {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error("Do not call MediaBackend directly: use a subclass")
|
new Error("Do not call MediaBackend directly: use a subclass"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds file to backend
|
* Adds file to backend
|
||||||
* @param file File to add
|
* @param file File to add
|
||||||
* @returns Metadata about the uploaded file
|
* @returns Metadata about the uploaded file
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public addFile(file: File): Promise<UploadedFileMetadata> {
|
public addFile(file: File): Promise<UploadedFileMetadata> {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error("Do not call MediaBackend directly: use a subclass")
|
new Error("Do not call MediaBackend directly: use a subclass"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,89 +6,89 @@
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
|
||||||
export enum ConvertableMediaFormats {
|
export enum ConvertableMediaFormats {
|
||||||
PNG = "png",
|
PNG = "png",
|
||||||
WEBP = "webp",
|
WEBP = "webp",
|
||||||
JPEG = "jpeg",
|
JPEG = "jpeg",
|
||||||
JPG = "jpg",
|
JPG = "jpg",
|
||||||
AVIF = "avif",
|
AVIF = "avif",
|
||||||
JXL = "jxl",
|
JXL = "jxl",
|
||||||
HEIF = "heif",
|
HEIF = "heif",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles media conversion between formats
|
* Handles media conversion between formats
|
||||||
*/
|
*/
|
||||||
export class MediaConverter {
|
export class MediaConverter {
|
||||||
constructor(
|
constructor(
|
||||||
public fromFormat: ConvertableMediaFormats,
|
public fromFormat: ConvertableMediaFormats,
|
||||||
public toFormat: ConvertableMediaFormats
|
public toFormat: ConvertableMediaFormats,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the media is convertable
|
* Returns whether the media is convertable
|
||||||
* @returns Whether the media is convertable
|
* @returns Whether the media is convertable
|
||||||
*/
|
*/
|
||||||
public isConvertable() {
|
public isConvertable() {
|
||||||
return (
|
return (
|
||||||
this.fromFormat !== this.toFormat &&
|
this.fromFormat !== this.toFormat &&
|
||||||
Object.values(ConvertableMediaFormats).includes(this.fromFormat)
|
Object.values(ConvertableMediaFormats).includes(this.fromFormat)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the file name with the extension replaced
|
* Returns the file name with the extension replaced
|
||||||
* @param fileName File name to replace
|
* @param fileName File name to replace
|
||||||
* @returns File name with extension replaced
|
* @returns File name with extension replaced
|
||||||
*/
|
*/
|
||||||
private getReplacedFileName(fileName: string) {
|
private getReplacedFileName(fileName: string) {
|
||||||
return this.extractFilenameFromPath(fileName).replace(
|
return this.extractFilenameFromPath(fileName).replace(
|
||||||
new RegExp(`\\.${this.fromFormat}$`),
|
new RegExp(`\\.${this.fromFormat}$`),
|
||||||
`.${this.toFormat}`
|
`.${this.toFormat}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the filename from a path
|
* Extracts the filename from a path
|
||||||
* @param path Path to extract filename from
|
* @param path Path to extract filename from
|
||||||
* @returns Extracted filename
|
* @returns Extracted filename
|
||||||
*/
|
*/
|
||||||
private extractFilenameFromPath(path: string) {
|
private extractFilenameFromPath(path: string) {
|
||||||
// Don't count escaped slashes as path separators
|
// Don't count escaped slashes as path separators
|
||||||
const pathParts = path.split(/(?<!\\)\//);
|
const pathParts = path.split(/(?<!\\)\//);
|
||||||
return pathParts[pathParts.length - 1];
|
return pathParts[pathParts.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts media to the specified format
|
* Converts media to the specified format
|
||||||
* @param media Media to convert
|
* @param media Media to convert
|
||||||
* @returns Converted media
|
* @returns Converted media
|
||||||
*/
|
*/
|
||||||
public async convert(media: File) {
|
public async convert(media: File) {
|
||||||
if (!this.isConvertable()) {
|
if (!this.isConvertable()) {
|
||||||
return media;
|
return media;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharpCommand = sharp(await media.arrayBuffer());
|
const sharpCommand = sharp(await media.arrayBuffer());
|
||||||
|
|
||||||
// Calculate newFilename before changing formats to prevent errors with jpg files
|
// Calculate newFilename before changing formats to prevent errors with jpg files
|
||||||
const newFilename = this.getReplacedFileName(media.name);
|
const newFilename = this.getReplacedFileName(media.name);
|
||||||
|
|
||||||
if (this.fromFormat === ConvertableMediaFormats.JPG) {
|
if (this.fromFormat === ConvertableMediaFormats.JPG) {
|
||||||
this.fromFormat = ConvertableMediaFormats.JPEG;
|
this.fromFormat = ConvertableMediaFormats.JPEG;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.toFormat === ConvertableMediaFormats.JPG) {
|
if (this.toFormat === ConvertableMediaFormats.JPG) {
|
||||||
this.toFormat = ConvertableMediaFormats.JPEG;
|
this.toFormat = ConvertableMediaFormats.JPEG;
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertedBuffer = await sharpCommand[this.toFormat]().toBuffer();
|
const convertedBuffer = await sharpCommand[this.toFormat]().toBuffer();
|
||||||
|
|
||||||
// Convert the buffer to a BlobPart
|
// Convert the buffer to a BlobPart
|
||||||
const buffer = new Blob([convertedBuffer]);
|
const buffer = new Blob([convertedBuffer]);
|
||||||
|
|
||||||
return new File([buffer], newFilename, {
|
return new File([buffer], newFilename, {
|
||||||
type: `image/${this.toFormat}`,
|
type: `image/${this.toFormat}`,
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "media-manager",
|
"name": "media-manager",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsr/bradenmacdonald__s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client",
|
"@jsr/bradenmacdonald__s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client",
|
||||||
"config-manager": "workspace:*"
|
"config-manager": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,276 +1,277 @@
|
||||||
|
import { beforeEach, describe, expect, it, jest, spyOn } from "bun:test";
|
||||||
|
import type { S3Client } from "@jsr/bradenmacdonald__s3-lite-client";
|
||||||
|
import type { Config } from "config-manager";
|
||||||
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/backends/s3.test.ts
|
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/backends/s3.test.ts
|
||||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||||
import type { S3Client } from "@bradenmacdonald/s3-lite-client";
|
|
||||||
import { beforeEach, describe, jest, it, expect, spyOn } from "bun:test";
|
|
||||||
import { S3MediaBackend } from "../backends/s3";
|
|
||||||
import type { ConfigType } from "config-manager";
|
|
||||||
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
|
|
||||||
import { LocalMediaBackend } from "../backends/local";
|
import { LocalMediaBackend } from "../backends/local";
|
||||||
|
import { S3MediaBackend } from "../backends/s3";
|
||||||
|
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
|
||||||
|
|
||||||
type DeepPartial<T> = {
|
type DeepPartial<T> = {
|
||||||
[P in keyof T]?: DeepPartial<T[P]>;
|
[P in keyof T]?: DeepPartial<T[P]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("MediaBackend", () => {
|
describe("MediaBackend", () => {
|
||||||
let mediaBackend: MediaBackend;
|
let mediaBackend: MediaBackend;
|
||||||
let mockConfig: ConfigType;
|
let mockConfig: Config;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
media: {
|
media: {
|
||||||
conversion: {
|
conversion: {
|
||||||
convert_images: true,
|
convert_images: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as ConfigType;
|
} as Config;
|
||||||
mediaBackend = new MediaBackend(mockConfig, MediaBackendType.S3);
|
mediaBackend = new MediaBackend(mockConfig, MediaBackendType.S3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize with correct backend type", () => {
|
it("should initialize with correct backend type", () => {
|
||||||
expect(mediaBackend.getBackendType()).toEqual(MediaBackendType.S3);
|
expect(mediaBackend.getBackendType()).toEqual(MediaBackendType.S3);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("fromBackendType", () => {
|
describe("fromBackendType", () => {
|
||||||
it("should return a LocalMediaBackend instance for LOCAL backend type", async () => {
|
it("should return a LocalMediaBackend instance for LOCAL backend type", async () => {
|
||||||
const backend = await MediaBackend.fromBackendType(
|
const backend = await MediaBackend.fromBackendType(
|
||||||
MediaBackendType.LOCAL,
|
MediaBackendType.LOCAL,
|
||||||
mockConfig
|
mockConfig,
|
||||||
);
|
);
|
||||||
expect(backend).toBeInstanceOf(LocalMediaBackend);
|
expect(backend).toBeInstanceOf(LocalMediaBackend);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a S3MediaBackend instance for S3 backend type", async () => {
|
it("should return a S3MediaBackend instance for S3 backend type", async () => {
|
||||||
const backend = await MediaBackend.fromBackendType(
|
const backend = await MediaBackend.fromBackendType(
|
||||||
MediaBackendType.S3,
|
MediaBackendType.S3,
|
||||||
{
|
{
|
||||||
s3: {
|
s3: {
|
||||||
endpoint: "localhost:4566",
|
endpoint: "localhost:4566",
|
||||||
region: "us-east-1",
|
region: "us-east-1",
|
||||||
bucket_name: "test-bucket",
|
bucket_name: "test-bucket",
|
||||||
access_key: "test-access",
|
access_key: "test-access",
|
||||||
public_url: "test",
|
public_url: "test",
|
||||||
secret_access_key: "test-secret",
|
secret_access_key: "test-secret",
|
||||||
},
|
},
|
||||||
} as ConfigType
|
} as Config,
|
||||||
);
|
);
|
||||||
expect(backend).toBeInstanceOf(S3MediaBackend);
|
expect(backend).toBeInstanceOf(S3MediaBackend);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw an error for unknown backend type", () => {
|
it("should throw an error for unknown backend type", () => {
|
||||||
expect(
|
expect(
|
||||||
MediaBackend.fromBackendType("unknown" as any, mockConfig)
|
// @ts-expect-error This is a test
|
||||||
).rejects.toThrow("Unknown backend type: unknown");
|
MediaBackend.fromBackendType("unknown", mockConfig),
|
||||||
});
|
).rejects.toThrow("Unknown backend type: unknown");
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should check if images should be converted", () => {
|
it("should check if images should be converted", () => {
|
||||||
expect(mediaBackend.shouldConvertImages(mockConfig)).toBe(true);
|
expect(mediaBackend.shouldConvertImages(mockConfig)).toBe(true);
|
||||||
mockConfig.media.conversion.convert_images = false;
|
mockConfig.media.conversion.convert_images = false;
|
||||||
expect(mediaBackend.shouldConvertImages(mockConfig)).toBe(false);
|
expect(mediaBackend.shouldConvertImages(mockConfig)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error when calling getFileByHash", () => {
|
it("should throw error when calling getFileByHash", () => {
|
||||||
const mockHash = "test-hash";
|
const mockHash = "test-hash";
|
||||||
const databaseHashFetcher = jest.fn().mockResolvedValue("test.jpg");
|
const databaseHashFetcher = jest.fn().mockResolvedValue("test.jpg");
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
mediaBackend.getFileByHash(mockHash, databaseHashFetcher)
|
mediaBackend.getFileByHash(mockHash, databaseHashFetcher),
|
||||||
).rejects.toThrow(Error);
|
).rejects.toThrow(Error);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error when calling getFile", () => {
|
it("should throw error when calling getFile", () => {
|
||||||
const mockFilename = "test.jpg";
|
const mockFilename = "test.jpg";
|
||||||
|
|
||||||
expect(mediaBackend.getFile(mockFilename)).rejects.toThrow(Error);
|
expect(mediaBackend.getFile(mockFilename)).rejects.toThrow(Error);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error when calling addFile", () => {
|
it("should throw error when calling addFile", () => {
|
||||||
const mockFile = new File([""], "test.jpg");
|
const mockFile = new File([""], "test.jpg");
|
||||||
|
|
||||||
expect(mediaBackend.addFile(mockFile)).rejects.toThrow();
|
expect(mediaBackend.addFile(mockFile)).rejects.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("S3MediaBackend", () => {
|
describe("S3MediaBackend", () => {
|
||||||
let s3MediaBackend: S3MediaBackend;
|
let s3MediaBackend: S3MediaBackend;
|
||||||
let mockS3Client: Partial<S3Client>;
|
let mockS3Client: Partial<S3Client>;
|
||||||
let mockConfig: DeepPartial<ConfigType>;
|
let mockConfig: DeepPartial<Config>;
|
||||||
let mockFile: File;
|
let mockFile: File;
|
||||||
let mockMediaHasher: MediaHasher;
|
let mockMediaHasher: MediaHasher;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
s3: {
|
s3: {
|
||||||
endpoint: "http://localhost:4566",
|
endpoint: "http://localhost:4566",
|
||||||
region: "us-east-1",
|
region: "us-east-1",
|
||||||
bucket_name: "test-bucket",
|
bucket_name: "test-bucket",
|
||||||
access_key: "test-access-key",
|
access_key: "test-access-key",
|
||||||
secret_access_key: "test-secret-access-key",
|
secret_access_key: "test-secret-access-key",
|
||||||
public_url: "test",
|
public_url: "test",
|
||||||
},
|
},
|
||||||
media: {
|
media: {
|
||||||
conversion: {
|
conversion: {
|
||||||
convert_to: ConvertableMediaFormats.PNG,
|
convert_to: ConvertableMediaFormats.PNG,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
mockFile = new File([new TextEncoder().encode("test")], "test.jpg");
|
mockFile = new File([new TextEncoder().encode("test")], "test.jpg");
|
||||||
mockMediaHasher = new MediaHasher();
|
mockMediaHasher = new MediaHasher();
|
||||||
mockS3Client = {
|
mockS3Client = {
|
||||||
putObject: jest.fn().mockResolvedValue({}),
|
putObject: jest.fn().mockResolvedValue({}),
|
||||||
statObject: jest.fn().mockResolvedValue({}),
|
statObject: jest.fn().mockResolvedValue({}),
|
||||||
getObject: jest.fn().mockResolvedValue({
|
getObject: jest.fn().mockResolvedValue({
|
||||||
blob: jest.fn().mockResolvedValue(new Blob()),
|
blob: jest.fn().mockResolvedValue(new Blob()),
|
||||||
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
||||||
}),
|
}),
|
||||||
} as Partial<S3Client>;
|
} as Partial<S3Client>;
|
||||||
s3MediaBackend = new S3MediaBackend(
|
s3MediaBackend = new S3MediaBackend(
|
||||||
mockConfig as ConfigType,
|
mockConfig as Config,
|
||||||
mockS3Client as S3Client
|
mockS3Client as S3Client,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize with correct type", () => {
|
it("should initialize with correct type", () => {
|
||||||
expect(s3MediaBackend.getBackendType()).toEqual(MediaBackendType.S3);
|
expect(s3MediaBackend.getBackendType()).toEqual(MediaBackendType.S3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add file", async () => {
|
it("should add file", async () => {
|
||||||
const mockHash = "test-hash";
|
const mockHash = "test-hash";
|
||||||
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
|
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
|
||||||
|
|
||||||
const result = await s3MediaBackend.addFile(mockFile);
|
const result = await s3MediaBackend.addFile(mockFile);
|
||||||
|
|
||||||
expect(result.uploadedFile).toEqual(mockFile);
|
expect(result.uploadedFile).toEqual(mockFile);
|
||||||
expect(result.hash).toHaveLength(64);
|
expect(result.hash).toHaveLength(64);
|
||||||
expect(mockS3Client.putObject).toHaveBeenCalledWith(
|
expect(mockS3Client.putObject).toHaveBeenCalledWith(
|
||||||
mockFile.name,
|
mockFile.name,
|
||||||
expect.any(ReadableStream),
|
expect.any(ReadableStream),
|
||||||
{ size: mockFile.size }
|
{ size: mockFile.size },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get file by hash", async () => {
|
it("should get file by hash", async () => {
|
||||||
const mockHash = "test-hash";
|
const mockHash = "test-hash";
|
||||||
const mockFilename = "test.jpg";
|
const mockFilename = "test.jpg";
|
||||||
const databaseHashFetcher = jest.fn().mockResolvedValue(mockFilename);
|
const databaseHashFetcher = jest.fn().mockResolvedValue(mockFilename);
|
||||||
mockS3Client.statObject = jest.fn().mockResolvedValue({});
|
mockS3Client.statObject = jest.fn().mockResolvedValue({});
|
||||||
mockS3Client.getObject = jest.fn().mockResolvedValue({
|
mockS3Client.getObject = jest.fn().mockResolvedValue({
|
||||||
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(10)),
|
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(10)),
|
||||||
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const file = await s3MediaBackend.getFileByHash(
|
const file = await s3MediaBackend.getFileByHash(
|
||||||
mockHash,
|
mockHash,
|
||||||
databaseHashFetcher
|
databaseHashFetcher,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(file).not.toBeNull();
|
expect(file).not.toBeNull();
|
||||||
expect(file?.name).toEqual(mockFilename);
|
expect(file?.name).toEqual(mockFilename);
|
||||||
expect(file?.type).toEqual("image/jpeg");
|
expect(file?.type).toEqual("image/jpeg");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get file", async () => {
|
it("should get file", async () => {
|
||||||
const mockFilename = "test.jpg";
|
const mockFilename = "test.jpg";
|
||||||
mockS3Client.statObject = jest.fn().mockResolvedValue({});
|
mockS3Client.statObject = jest.fn().mockResolvedValue({});
|
||||||
mockS3Client.getObject = jest.fn().mockResolvedValue({
|
mockS3Client.getObject = jest.fn().mockResolvedValue({
|
||||||
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(10)),
|
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(10)),
|
||||||
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const file = await s3MediaBackend.getFile(mockFilename);
|
const file = await s3MediaBackend.getFile(mockFilename);
|
||||||
|
|
||||||
expect(file).not.toBeNull();
|
expect(file).not.toBeNull();
|
||||||
expect(file?.name).toEqual(mockFilename);
|
expect(file?.name).toEqual(mockFilename);
|
||||||
expect(file?.type).toEqual("image/jpeg");
|
expect(file?.type).toEqual("image/jpeg");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("LocalMediaBackend", () => {
|
describe("LocalMediaBackend", () => {
|
||||||
let localMediaBackend: LocalMediaBackend;
|
let localMediaBackend: LocalMediaBackend;
|
||||||
let mockConfig: ConfigType;
|
let mockConfig: Config;
|
||||||
let mockFile: File;
|
let mockFile: File;
|
||||||
let mockMediaHasher: MediaHasher;
|
let mockMediaHasher: MediaHasher;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
media: {
|
media: {
|
||||||
conversion: {
|
conversion: {
|
||||||
convert_images: true,
|
convert_images: true,
|
||||||
convert_to: ConvertableMediaFormats.PNG,
|
convert_to: ConvertableMediaFormats.PNG,
|
||||||
},
|
},
|
||||||
local_uploads_folder: "./uploads",
|
local_uploads_folder: "./uploads",
|
||||||
},
|
},
|
||||||
} as ConfigType;
|
} as Config;
|
||||||
mockFile = Bun.file(__dirname + "/megamind.jpg") as unknown as File;
|
mockFile = Bun.file(`${__dirname}/megamind.jpg`) as unknown as File;
|
||||||
mockMediaHasher = new MediaHasher();
|
mockMediaHasher = new MediaHasher();
|
||||||
localMediaBackend = new LocalMediaBackend(mockConfig);
|
localMediaBackend = new LocalMediaBackend(mockConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize with correct type", () => {
|
it("should initialize with correct type", () => {
|
||||||
expect(localMediaBackend.getBackendType()).toEqual(
|
expect(localMediaBackend.getBackendType()).toEqual(
|
||||||
MediaBackendType.LOCAL
|
MediaBackendType.LOCAL,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add file", async () => {
|
it("should add file", async () => {
|
||||||
const mockHash = "test-hash";
|
const mockHash = "test-hash";
|
||||||
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
|
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
|
||||||
const mockMediaConverter = new MediaConverter(
|
const mockMediaConverter = new MediaConverter(
|
||||||
ConvertableMediaFormats.JPG,
|
ConvertableMediaFormats.JPG,
|
||||||
ConvertableMediaFormats.PNG
|
ConvertableMediaFormats.PNG,
|
||||||
);
|
);
|
||||||
spyOn(mockMediaConverter, "convert").mockResolvedValue(mockFile);
|
spyOn(mockMediaConverter, "convert").mockResolvedValue(mockFile);
|
||||||
// @ts-expect-error This is a mock
|
// @ts-expect-error This is a mock
|
||||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||||
exists: () => Promise.resolve(false),
|
exists: () => Promise.resolve(false),
|
||||||
}));
|
}));
|
||||||
spyOn(Bun, "write").mockImplementationOnce(() =>
|
spyOn(Bun, "write").mockImplementationOnce(() =>
|
||||||
Promise.resolve(mockFile.size)
|
Promise.resolve(mockFile.size),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await localMediaBackend.addFile(mockFile);
|
const result = await localMediaBackend.addFile(mockFile);
|
||||||
|
|
||||||
expect(result.uploadedFile).toEqual(mockFile);
|
expect(result.uploadedFile).toEqual(mockFile);
|
||||||
expect(result.path).toEqual(`./uploads/megamind.png`);
|
expect(result.path).toEqual("./uploads/megamind.png");
|
||||||
expect(result.hash).toHaveLength(64);
|
expect(result.hash).toHaveLength(64);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get file by hash", async () => {
|
it("should get file by hash", async () => {
|
||||||
const mockHash = "test-hash";
|
const mockHash = "test-hash";
|
||||||
const mockFilename = "test.jpg";
|
const mockFilename = "test.jpg";
|
||||||
const databaseHashFetcher = jest.fn().mockResolvedValue(mockFilename);
|
const databaseHashFetcher = jest.fn().mockResolvedValue(mockFilename);
|
||||||
// @ts-expect-error This is a mock
|
// @ts-expect-error This is a mock
|
||||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||||
exists: () => Promise.resolve(true),
|
exists: () => Promise.resolve(true),
|
||||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
||||||
type: "image/jpeg",
|
type: "image/jpeg",
|
||||||
lastModified: 123456789,
|
lastModified: 123456789,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const file = await localMediaBackend.getFileByHash(
|
const file = await localMediaBackend.getFileByHash(
|
||||||
mockHash,
|
mockHash,
|
||||||
databaseHashFetcher
|
databaseHashFetcher,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(file).not.toBeNull();
|
expect(file).not.toBeNull();
|
||||||
expect(file?.name).toEqual(mockFilename);
|
expect(file?.name).toEqual(mockFilename);
|
||||||
expect(file?.type).toEqual("image/jpeg");
|
expect(file?.type).toEqual("image/jpeg");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get file", async () => {
|
it("should get file", async () => {
|
||||||
const mockFilename = "test.jpg";
|
const mockFilename = "test.jpg";
|
||||||
// @ts-expect-error This is a mock
|
// @ts-expect-error This is a mock
|
||||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||||
exists: () => Promise.resolve(true),
|
exists: () => Promise.resolve(true),
|
||||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
||||||
type: "image/jpeg",
|
type: "image/jpeg",
|
||||||
lastModified: 123456789,
|
lastModified: 123456789,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const file = await localMediaBackend.getFile(mockFilename);
|
const file = await localMediaBackend.getFile(mockFilename);
|
||||||
|
|
||||||
expect(file).not.toBeNull();
|
expect(file).not.toBeNull();
|
||||||
expect(file?.name).toEqual(mockFilename);
|
expect(file?.name).toEqual(mockFilename);
|
||||||
expect(file?.type).toEqual("image/jpeg");
|
expect(file?.type).toEqual("image/jpeg");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,65 @@
|
||||||
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/media-converter.test.ts
|
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/media-converter.test.ts
|
||||||
import { describe, it, expect, beforeEach } from "bun:test";
|
import { beforeEach, describe, expect, it } from "bun:test";
|
||||||
import { MediaConverter, ConvertableMediaFormats } from "../media-converter";
|
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
|
||||||
|
|
||||||
describe("MediaConverter", () => {
|
describe("MediaConverter", () => {
|
||||||
let mediaConverter: MediaConverter;
|
let mediaConverter: MediaConverter;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mediaConverter = new MediaConverter(
|
mediaConverter = new MediaConverter(
|
||||||
ConvertableMediaFormats.JPG,
|
ConvertableMediaFormats.JPG,
|
||||||
ConvertableMediaFormats.PNG
|
ConvertableMediaFormats.PNG,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize with correct formats", () => {
|
it("should initialize with correct formats", () => {
|
||||||
expect(mediaConverter.fromFormat).toEqual(ConvertableMediaFormats.JPG);
|
expect(mediaConverter.fromFormat).toEqual(ConvertableMediaFormats.JPG);
|
||||||
expect(mediaConverter.toFormat).toEqual(ConvertableMediaFormats.PNG);
|
expect(mediaConverter.toFormat).toEqual(ConvertableMediaFormats.PNG);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should check if media is convertable", () => {
|
it("should check if media is convertable", () => {
|
||||||
expect(mediaConverter.isConvertable()).toBe(true);
|
expect(mediaConverter.isConvertable()).toBe(true);
|
||||||
mediaConverter.toFormat = ConvertableMediaFormats.JPG;
|
mediaConverter.toFormat = ConvertableMediaFormats.JPG;
|
||||||
expect(mediaConverter.isConvertable()).toBe(false);
|
expect(mediaConverter.isConvertable()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should replace file name extension", () => {
|
it("should replace file name extension", () => {
|
||||||
const fileName = "test.jpg";
|
const fileName = "test.jpg";
|
||||||
const expectedFileName = "test.png";
|
const expectedFileName = "test.png";
|
||||||
// Written like this because it's a private function
|
// Written like this because it's a private function
|
||||||
expect(mediaConverter["getReplacedFileName"](fileName)).toEqual(
|
expect(mediaConverter.getReplacedFileName(fileName)).toEqual(
|
||||||
expectedFileName
|
expectedFileName,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Filename extractor", () => {
|
describe("Filename extractor", () => {
|
||||||
it("should extract filename from path", () => {
|
it("should extract filename from path", () => {
|
||||||
const path = "path/to/test.jpg";
|
const path = "path/to/test.jpg";
|
||||||
const expectedFileName = "test.jpg";
|
const expectedFileName = "test.jpg";
|
||||||
expect(mediaConverter["extractFilenameFromPath"](path)).toEqual(
|
expect(mediaConverter.extractFilenameFromPath(path)).toEqual(
|
||||||
expectedFileName
|
expectedFileName,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle escaped slashes", () => {
|
it("should handle escaped slashes", () => {
|
||||||
const path = "path/to/test\\/test.jpg";
|
const path = "path/to/test\\/test.jpg";
|
||||||
const expectedFileName = "test\\/test.jpg";
|
const expectedFileName = "test\\/test.jpg";
|
||||||
expect(mediaConverter["extractFilenameFromPath"](path)).toEqual(
|
expect(mediaConverter.extractFilenameFromPath(path)).toEqual(
|
||||||
expectedFileName
|
expectedFileName,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should convert media", async () => {
|
it("should convert media", async () => {
|
||||||
const file = Bun.file(__dirname + "/megamind.jpg");
|
const file = Bun.file(`${__dirname}/megamind.jpg`);
|
||||||
|
|
||||||
const convertedFile = await mediaConverter.convert(
|
const convertedFile = await mediaConverter.convert(
|
||||||
file as unknown as File
|
file as unknown as File,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(convertedFile.name).toEqual("megamind.png");
|
expect(convertedFile.name).toEqual("megamind.png");
|
||||||
expect(convertedFile.type).toEqual(
|
expect(convertedFile.type).toEqual(
|
||||||
`image/${ConvertableMediaFormats.PNG}`
|
`image/${ConvertableMediaFormats.PNG}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import type { APActor, APNote } from "activitypub-types";
|
||||||
import { ActivityPubTranslator } from "./protocols/activitypub";
|
import { ActivityPubTranslator } from "./protocols/activitypub";
|
||||||
|
|
||||||
export enum SupportedProtocols {
|
export enum SupportedProtocols {
|
||||||
ACTIVITYPUB = "activitypub",
|
ACTIVITYPUB = "activitypub",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -12,37 +12,40 @@ export enum SupportedProtocols {
|
||||||
* This class is not meant to be instantiated directly, but rather for its children to be used.
|
* This class is not meant to be instantiated directly, but rather for its children to be used.
|
||||||
*/
|
*/
|
||||||
export class ProtocolTranslator {
|
export class ProtocolTranslator {
|
||||||
static auto(object: any) {
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
const protocol = this.recognizeProtocol(object);
|
static auto(object: any) {
|
||||||
switch (protocol) {
|
const protocol = ProtocolTranslator.recognizeProtocol(object);
|
||||||
case SupportedProtocols.ACTIVITYPUB:
|
switch (protocol) {
|
||||||
return new ActivityPubTranslator();
|
case SupportedProtocols.ACTIVITYPUB:
|
||||||
default:
|
return new ActivityPubTranslator();
|
||||||
throw new Error("Unknown protocol");
|
default:
|
||||||
}
|
throw new Error("Unknown protocol");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates an ActivityPub actor to a Lysand user
|
* Translates an ActivityPub actor to a Lysand user
|
||||||
* @param data Raw JSON-LD data from an ActivityPub actor
|
* @param data Raw JSON-LD data from an ActivityPub actor
|
||||||
*/
|
*/
|
||||||
user(data: APActor) {
|
user(data: APActor) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates an ActivityPub note to a Lysand status
|
* Translates an ActivityPub note to a Lysand status
|
||||||
* @param data Raw JSON-LD data from an ActivityPub note
|
* @param data Raw JSON-LD data from an ActivityPub note
|
||||||
*/
|
*/
|
||||||
status(data: APNote) {
|
status(data: APNote) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically recognizes the protocol of a given object
|
* Automatically recognizes the protocol of a given object
|
||||||
*/
|
*/
|
||||||
private static recognizeProtocol(object: any) {
|
|
||||||
// Temporary stub
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
return SupportedProtocols.ACTIVITYPUB;
|
private static recognizeProtocol(object: any) {
|
||||||
}
|
// Temporary stub
|
||||||
|
return SupportedProtocols.ACTIVITYPUB;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "protocol-translator",
|
"name": "protocol-translator",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"activitypub-types": "^1.1.0"
|
"activitypub-types": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import { ProtocolTranslator } from "..";
|
import { ProtocolTranslator } from "..";
|
||||||
|
|
||||||
export class ActivityPubTranslator extends ProtocolTranslator {
|
export class ActivityPubTranslator extends ProtocolTranslator {
|
||||||
constructor() {
|
user() {}
|
||||||
super();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
user() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -13,158 +13,158 @@
|
||||||
* @returns JavaScript object of type T
|
* @returns JavaScript object of type T
|
||||||
*/
|
*/
|
||||||
export class RequestParser {
|
export class RequestParser {
|
||||||
constructor(public request: Request) {}
|
constructor(public request: Request) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse request body into a JavaScript object
|
* Parse request body into a JavaScript object
|
||||||
* @returns JavaScript object of type T
|
* @returns JavaScript object of type T
|
||||||
* @throws Error if body is invalid
|
* @throws Error if body is invalid
|
||||||
*/
|
*/
|
||||||
async toObject<T>() {
|
async toObject<T>() {
|
||||||
try {
|
try {
|
||||||
switch (await this.determineContentType()) {
|
switch (await this.determineContentType()) {
|
||||||
case "application/json":
|
case "application/json":
|
||||||
return this.parseJson<T>();
|
return this.parseJson<T>();
|
||||||
case "application/x-www-form-urlencoded":
|
case "application/x-www-form-urlencoded":
|
||||||
return this.parseFormUrlencoded<T>();
|
return this.parseFormUrlencoded<T>();
|
||||||
case "multipart/form-data":
|
case "multipart/form-data":
|
||||||
return this.parseFormData<T>();
|
return this.parseFormData<T>();
|
||||||
default:
|
default:
|
||||||
return this.parseQuery<T>();
|
return this.parseQuery<T>();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return {} as T;
|
return {} as T;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine body content type
|
* Determine body content type
|
||||||
* If there is no Content-Type header, automatically
|
* If there is no Content-Type header, automatically
|
||||||
* guess content type. Cuts off after ";" character
|
* guess content type. Cuts off after ";" character
|
||||||
* @returns Content-Type header value, or empty string if there is no body
|
* @returns Content-Type header value, or empty string if there is no body
|
||||||
* @throws Error if body is invalid
|
* @throws Error if body is invalid
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async determineContentType() {
|
private async determineContentType() {
|
||||||
if (this.request.headers.get("Content-Type")) {
|
if (this.request.headers.get("Content-Type")) {
|
||||||
return (
|
return (
|
||||||
this.request.headers.get("Content-Type")?.split(";")[0] ?? ""
|
this.request.headers.get("Content-Type")?.split(";")[0] ?? ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if body is valid JSON
|
// Check if body is valid JSON
|
||||||
try {
|
try {
|
||||||
await this.request.json();
|
await this.request.json();
|
||||||
return "application/json";
|
return "application/json";
|
||||||
} catch {
|
} catch {
|
||||||
// This is not JSON
|
// This is not JSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if body is valid FormData
|
// Check if body is valid FormData
|
||||||
try {
|
try {
|
||||||
await this.request.formData();
|
await this.request.formData();
|
||||||
return "multipart/form-data";
|
return "multipart/form-data";
|
||||||
} catch {
|
} catch {
|
||||||
// This is not FormData
|
// This is not FormData
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.request.body) {
|
if (this.request.body) {
|
||||||
throw new Error("Invalid body");
|
throw new Error("Invalid body");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no body, return query parameters
|
// If there is no body, return query parameters
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse FormData body into a JavaScript object
|
* Parse FormData body into a JavaScript object
|
||||||
* @returns JavaScript object of type T
|
* @returns JavaScript object of type T
|
||||||
* @private
|
* @private
|
||||||
* @throws Error if body is invalid
|
* @throws Error if body is invalid
|
||||||
*/
|
*/
|
||||||
private async parseFormData<T>(): Promise<Partial<T>> {
|
private async parseFormData<T>(): Promise<Partial<T>> {
|
||||||
const formData = await this.request.formData();
|
const formData = await this.request.formData();
|
||||||
const result: Partial<T> = {};
|
const result: Partial<T> = {};
|
||||||
|
|
||||||
for (const [key, value] of formData.entries()) {
|
for (const [key, value] of formData.entries()) {
|
||||||
if (value instanceof File) {
|
if (value instanceof File) {
|
||||||
result[key as keyof T] = value as any;
|
result[key as keyof T] = value as T[keyof T];
|
||||||
} else if (key.endsWith("[]")) {
|
} else if (key.endsWith("[]")) {
|
||||||
const arrayKey = key.slice(0, -2) as keyof T;
|
const arrayKey = key.slice(0, -2) as keyof T;
|
||||||
if (!result[arrayKey]) {
|
if (!result[arrayKey]) {
|
||||||
result[arrayKey] = [] as T[keyof T];
|
result[arrayKey] = [] as T[keyof T];
|
||||||
}
|
}
|
||||||
|
|
||||||
(result[arrayKey] as any[]).push(value);
|
(result[arrayKey] as FormDataEntryValue[]).push(value);
|
||||||
} else {
|
} else {
|
||||||
result[key as keyof T] = value as any;
|
result[key as keyof T] = value as T[keyof T];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse application/x-www-form-urlencoded body into a JavaScript object
|
* Parse application/x-www-form-urlencoded body into a JavaScript object
|
||||||
* @returns JavaScript object of type T
|
* @returns JavaScript object of type T
|
||||||
* @private
|
* @private
|
||||||
* @throws Error if body is invalid
|
* @throws Error if body is invalid
|
||||||
*/
|
*/
|
||||||
private async parseFormUrlencoded<T>(): Promise<Partial<T>> {
|
private async parseFormUrlencoded<T>(): Promise<Partial<T>> {
|
||||||
const formData = await this.request.formData();
|
const formData = await this.request.formData();
|
||||||
const result: Partial<T> = {};
|
const result: Partial<T> = {};
|
||||||
|
|
||||||
for (const [key, value] of formData.entries()) {
|
for (const [key, value] of formData.entries()) {
|
||||||
if (key.endsWith("[]")) {
|
if (key.endsWith("[]")) {
|
||||||
const arrayKey = key.slice(0, -2) as keyof T;
|
const arrayKey = key.slice(0, -2) as keyof T;
|
||||||
if (!result[arrayKey]) {
|
if (!result[arrayKey]) {
|
||||||
result[arrayKey] = [] as T[keyof T];
|
result[arrayKey] = [] as T[keyof T];
|
||||||
}
|
}
|
||||||
|
|
||||||
(result[arrayKey] as any[]).push(value);
|
(result[arrayKey] as FormDataEntryValue[]).push(value);
|
||||||
} else {
|
} else {
|
||||||
result[key as keyof T] = value as any;
|
result[key as keyof T] = value as T[keyof T];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse JSON body into a JavaScript object
|
* Parse JSON body into a JavaScript object
|
||||||
* @returns JavaScript object of type T
|
* @returns JavaScript object of type T
|
||||||
* @private
|
* @private
|
||||||
* @throws Error if body is invalid
|
* @throws Error if body is invalid
|
||||||
*/
|
*/
|
||||||
private async parseJson<T>(): Promise<Partial<T>> {
|
private async parseJson<T>(): Promise<Partial<T>> {
|
||||||
try {
|
try {
|
||||||
return (await this.request.json()) as T;
|
return (await this.request.json()) as T;
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse query parameters into a JavaScript object
|
* Parse query parameters into a JavaScript object
|
||||||
* @private
|
* @private
|
||||||
* @throws Error if body is invalid
|
* @throws Error if body is invalid
|
||||||
* @returns JavaScript object of type T
|
* @returns JavaScript object of type T
|
||||||
*/
|
*/
|
||||||
private parseQuery<T>(): Partial<T> {
|
private parseQuery<T>(): Partial<T> {
|
||||||
const result: Partial<T> = {};
|
const result: Partial<T> = {};
|
||||||
const url = new URL(this.request.url);
|
const url = new URL(this.request.url);
|
||||||
|
|
||||||
for (const [key, value] of url.searchParams.entries()) {
|
for (const [key, value] of url.searchParams.entries()) {
|
||||||
if (key.endsWith("[]")) {
|
if (key.endsWith("[]")) {
|
||||||
const arrayKey = key.slice(0, -2) as keyof T;
|
const arrayKey = key.slice(0, -2) as keyof T;
|
||||||
if (!result[arrayKey]) {
|
if (!result[arrayKey]) {
|
||||||
result[arrayKey] = [] as T[keyof T];
|
result[arrayKey] = [] as T[keyof T];
|
||||||
}
|
}
|
||||||
(result[arrayKey] as string[]).push(value);
|
(result[arrayKey] as string[]).push(value);
|
||||||
} else {
|
} else {
|
||||||
result[key as keyof T] = value as any;
|
result[key as keyof T] = value as T[keyof T];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,158 +1,158 @@
|
||||||
import { describe, it, expect, test } from "bun:test";
|
import { describe, expect, it, test } from "bun:test";
|
||||||
import { RequestParser } from "..";
|
import { RequestParser } from "..";
|
||||||
|
|
||||||
describe("RequestParser", () => {
|
describe("RequestParser", () => {
|
||||||
describe("Should parse query parameters correctly", () => {
|
describe("Should parse query parameters correctly", () => {
|
||||||
test("With text parameters", async () => {
|
test("With text parameters", async () => {
|
||||||
const request = new Request(
|
const request = new Request(
|
||||||
"http://localhost?param1=value1¶m2=value2"
|
"http://localhost?param1=value1¶m2=value2",
|
||||||
);
|
);
|
||||||
const result = await new RequestParser(request).toObject<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
param1: string;
|
param1: string;
|
||||||
param2: string;
|
param2: string;
|
||||||
}>();
|
}>();
|
||||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("With Array", async () => {
|
test("With Array", async () => {
|
||||||
const request = new Request(
|
const request = new Request(
|
||||||
"http://localhost?test[]=value1&test[]=value2"
|
"http://localhost?test[]=value1&test[]=value2",
|
||||||
);
|
);
|
||||||
const result = await new RequestParser(request).toObject<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
test: string[];
|
test: string[];
|
||||||
}>();
|
}>();
|
||||||
expect(result.test).toEqual(["value1", "value2"]);
|
expect(result.test).toEqual(["value1", "value2"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("With both at once", async () => {
|
test("With both at once", async () => {
|
||||||
const request = new Request(
|
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<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
param1: string;
|
param1: string;
|
||||||
param2: string;
|
param2: string;
|
||||||
test: string[];
|
test: string[];
|
||||||
}>();
|
}>();
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
param1: "value1",
|
param1: "value1",
|
||||||
param2: "value2",
|
param2: "value2",
|
||||||
test: ["value1", "value2"],
|
test: ["value1", "value2"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse JSON body correctly", async () => {
|
it("should parse JSON body correctly", async () => {
|
||||||
const request = new Request("http://localhost", {
|
const request = new Request("http://localhost", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ param1: "value1", param2: "value2" }),
|
body: JSON.stringify({ param1: "value1", param2: "value2" }),
|
||||||
});
|
});
|
||||||
const result = await new RequestParser(request).toObject<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
param1: string;
|
param1: string;
|
||||||
param2: string;
|
param2: string;
|
||||||
}>();
|
}>();
|
||||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle invalid JSON body", async () => {
|
it("should handle invalid JSON body", async () => {
|
||||||
const request = new Request("http://localhost", {
|
const request = new Request("http://localhost", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: "invalid json",
|
body: "invalid json",
|
||||||
});
|
});
|
||||||
const result = await new RequestParser(request).toObject<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
param1: string;
|
param1: string;
|
||||||
param2: string;
|
param2: string;
|
||||||
}>();
|
}>();
|
||||||
expect(result).toEqual({});
|
expect(result).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should parse form data correctly", () => {
|
describe("should parse form data correctly", () => {
|
||||||
test("With basic text parameters", async () => {
|
test("With basic text parameters", async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("param1", "value1");
|
formData.append("param1", "value1");
|
||||||
formData.append("param2", "value2");
|
formData.append("param2", "value2");
|
||||||
const request = new Request("http://localhost", {
|
const request = new Request("http://localhost", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
const result = await new RequestParser(request).toObject<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
param1: string;
|
param1: string;
|
||||||
param2: string;
|
param2: string;
|
||||||
}>();
|
}>();
|
||||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("With File object", async () => {
|
test("With File object", async () => {
|
||||||
const file = new File(["content"], "filename.txt", {
|
const file = new File(["content"], "filename.txt", {
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
});
|
});
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
const request = new Request("http://localhost", {
|
const request = new Request("http://localhost", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
const result = await new RequestParser(request).toObject<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
file: File;
|
file: File;
|
||||||
}>();
|
}>();
|
||||||
expect(result.file).toBeInstanceOf(File);
|
expect(result.file).toBeInstanceOf(File);
|
||||||
expect(await result.file?.text()).toEqual("content");
|
expect(await result.file?.text()).toEqual("content");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("With Array", async () => {
|
test("With Array", async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("test[]", "value1");
|
formData.append("test[]", "value1");
|
||||||
formData.append("test[]", "value2");
|
formData.append("test[]", "value2");
|
||||||
const request = new Request("http://localhost", {
|
const request = new Request("http://localhost", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
const result = await new RequestParser(request).toObject<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
test: string[];
|
test: string[];
|
||||||
}>();
|
}>();
|
||||||
expect(result.test).toEqual(["value1", "value2"]);
|
expect(result.test).toEqual(["value1", "value2"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("With all three at once", async () => {
|
test("With all three at once", async () => {
|
||||||
const file = new File(["content"], "filename.txt", {
|
const file = new File(["content"], "filename.txt", {
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
});
|
});
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("param1", "value1");
|
formData.append("param1", "value1");
|
||||||
formData.append("param2", "value2");
|
formData.append("param2", "value2");
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("test[]", "value1");
|
formData.append("test[]", "value1");
|
||||||
formData.append("test[]", "value2");
|
formData.append("test[]", "value2");
|
||||||
const request = new Request("http://localhost", {
|
const request = new Request("http://localhost", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
const result = await new RequestParser(request).toObject<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
param1: string;
|
param1: string;
|
||||||
param2: string;
|
param2: string;
|
||||||
file: File;
|
file: File;
|
||||||
test: string[];
|
test: string[];
|
||||||
}>();
|
}>();
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
param1: "value1",
|
param1: "value1",
|
||||||
param2: "value2",
|
param2: "value2",
|
||||||
file: file,
|
file: file,
|
||||||
test: ["value1", "value2"],
|
test: ["value1", "value2"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("URL Encoded", async () => {
|
test("URL Encoded", async () => {
|
||||||
const request = new Request("http://localhost", {
|
const request = new Request("http://localhost", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
},
|
},
|
||||||
body: "param1=value1¶m2=value2",
|
body: "param1=value1¶m2=value2",
|
||||||
});
|
});
|
||||||
const result = await new RequestParser(request).toObject<{
|
const result = await new RequestParser(request).toObject<{
|
||||||
param1: string;
|
param1: string;
|
||||||
param2: string;
|
param2: string;
|
||||||
}>();
|
}>();
|
||||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
// Import Tailwind style reset
|
// Import Tailwind style reset
|
||||||
import '@unocss/reset/tailwind-compat.css'
|
import "@unocss/reset/tailwind-compat.css";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -26,5 +26,5 @@ const checkValid = (e: Event) => {
|
||||||
} else {
|
} else {
|
||||||
isInvalid.value = true;
|
isInvalid.value = true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { createApp } from "vue";
|
|
||||||
import "./style.css";
|
|
||||||
import "virtual:uno.css";
|
import "virtual:uno.css";
|
||||||
|
import { createApp } from "vue";
|
||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: routes,
|
routes: routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
|
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
const version = __VERSION__;
|
const version = __VERSION__;
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from 'vue-router';
|
import { onMounted, ref } from "vue";
|
||||||
import LoginInput from "../../components/LoginInput.vue"
|
import { useRoute } from "vue-router";
|
||||||
import { onMounted, ref } from 'vue';
|
import LoginInput from "../../components/LoginInput.vue";
|
||||||
|
|
||||||
const query = useRoute().query;
|
const query = useRoute().query;
|
||||||
|
|
||||||
|
|
@ -64,18 +64,21 @@ const client_id = query.client_id;
|
||||||
const scope = query.scope;
|
const scope = query.scope;
|
||||||
const error = decodeURIComponent(query.error as string);
|
const error = decodeURIComponent(query.error as string);
|
||||||
|
|
||||||
const oauthProviders = ref<{
|
const oauthProviders = ref<
|
||||||
name: string;
|
| {
|
||||||
icon: string;
|
name: string;
|
||||||
id: string
|
icon: string;
|
||||||
}[] | null>(null);
|
id: string;
|
||||||
|
}[]
|
||||||
|
| null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const getOauthProviders = async () => {
|
const getOauthProviders = async () => {
|
||||||
const response = await fetch('/oauth/providers');
|
const response = await fetch("/oauth/providers");
|
||||||
return await response.json() as any;
|
return await response.json();
|
||||||
}
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
oauthProviders.value = await getOauthProviders();
|
oauthProviders.value = await getOauthProviders();
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
const query = useRoute().query;
|
const query = useRoute().query;
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ const application = query.application;
|
||||||
const website = decodeURIComponent(query.website as string);
|
const website = decodeURIComponent(query.website as string);
|
||||||
const redirect_uri = query.redirect_uri as string;
|
const redirect_uri = query.redirect_uri as string;
|
||||||
const client_id = query.client_id;
|
const client_id = query.client_id;
|
||||||
const scope = decodeURIComponent(query.scope as string || "");
|
const scope = decodeURIComponent((query.scope as string) || "");
|
||||||
const code = query.code;
|
const code = query.code;
|
||||||
|
|
||||||
const oauthScopeText: Record<string, string> = {
|
const oauthScopeText: Record<string, string> = {
|
||||||
|
|
@ -79,7 +79,7 @@ const oauthScopeText: Record<string, string> = {
|
||||||
"w:conversations": "Edit your conversations",
|
"w:conversations": "Edit your conversations",
|
||||||
"w:media": "Upload media",
|
"w:media": "Upload media",
|
||||||
"w:reports": "Report users",
|
"w:reports": "Report users",
|
||||||
}
|
};
|
||||||
|
|
||||||
const scopes = scope.split(" ");
|
const scopes = scope.split(" ");
|
||||||
|
|
||||||
|
|
@ -89,30 +89,56 @@ const scopes = scope.split(" ");
|
||||||
// Return an array of strings to display
|
// Return an array of strings to display
|
||||||
// "read write:accounts" returns all the fields with $VERB as read, plus the accounts field with $VERB as write
|
// "read write:accounts" returns all the fields with $VERB as read, plus the accounts field with $VERB as write
|
||||||
const getScopeText = (fullScopes: string[]) => {
|
const getScopeText = (fullScopes: string[]) => {
|
||||||
let scopeTexts = [];
|
const scopeTexts = [];
|
||||||
|
|
||||||
const readScopes = fullScopes.filter(scope => scope.includes("read"));
|
const readScopes = fullScopes.filter((scope) => scope.includes("read"));
|
||||||
const writeScopes = fullScopes.filter(scope => scope.includes("write"));
|
const writeScopes = fullScopes.filter((scope) => scope.includes("write"));
|
||||||
|
|
||||||
for (const possibleScope of Object.keys(oauthScopeText)) {
|
for (const possibleScope of Object.keys(oauthScopeText)) {
|
||||||
const [scopeAction, scopeName] = possibleScope.split(':');
|
const [scopeAction, scopeName] = possibleScope.split(":");
|
||||||
|
|
||||||
if (scopeAction.includes("rw") && (readScopes.includes(`read:${scopeName}`) || readScopes.find(scope => scope === "read")) && (writeScopes.includes(`write:${scopeName}`) || writeScopes.find(scope => scope === "write"))) {
|
if (
|
||||||
if (oauthScopeText[possibleScope].includes("$VERB")) scopeTexts.push(["Read and write", oauthScopeText[possibleScope].replace("$VERB", "")]);
|
scopeAction.includes("rw") &&
|
||||||
|
(readScopes.includes(`read:${scopeName}`) ||
|
||||||
|
readScopes.find((scope) => scope === "read")) &&
|
||||||
|
(writeScopes.includes(`write:${scopeName}`) ||
|
||||||
|
writeScopes.find((scope) => scope === "write"))
|
||||||
|
) {
|
||||||
|
if (oauthScopeText[possibleScope].includes("$VERB"))
|
||||||
|
scopeTexts.push([
|
||||||
|
"Read and write",
|
||||||
|
oauthScopeText[possibleScope].replace("$VERB", ""),
|
||||||
|
]);
|
||||||
else scopeTexts.push(["", oauthScopeText[possibleScope]]);
|
else scopeTexts.push(["", oauthScopeText[possibleScope]]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scopeAction.includes('r') && (readScopes.includes(`read:${scopeName}`) || readScopes.find(scope => scope === "read"))) {
|
if (
|
||||||
if (oauthScopeText[possibleScope].includes("$VERB")) scopeTexts.push(["Read", oauthScopeText[possibleScope].replace("$VERB", "")]);
|
scopeAction.includes("r") &&
|
||||||
|
(readScopes.includes(`read:${scopeName}`) ||
|
||||||
|
readScopes.find((scope) => scope === "read"))
|
||||||
|
) {
|
||||||
|
if (oauthScopeText[possibleScope].includes("$VERB"))
|
||||||
|
scopeTexts.push([
|
||||||
|
"Read",
|
||||||
|
oauthScopeText[possibleScope].replace("$VERB", ""),
|
||||||
|
]);
|
||||||
else scopeTexts.push(["", oauthScopeText[possibleScope]]);
|
else scopeTexts.push(["", oauthScopeText[possibleScope]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scopeAction.includes('w') && (writeScopes.includes(`write:${scopeName}`) || writeScopes.find(scope => scope === "write"))) {
|
if (
|
||||||
if (oauthScopeText[possibleScope].includes("$VERB")) scopeTexts.push(["Write", oauthScopeText[possibleScope].replace("$VERB", "")]);
|
scopeAction.includes("w") &&
|
||||||
|
(writeScopes.includes(`write:${scopeName}`) ||
|
||||||
|
writeScopes.find((scope) => scope === "write"))
|
||||||
|
) {
|
||||||
|
if (oauthScopeText[possibleScope].includes("$VERB"))
|
||||||
|
scopeTexts.push([
|
||||||
|
"Write",
|
||||||
|
oauthScopeText[possibleScope].replace("$VERB", ""),
|
||||||
|
]);
|
||||||
else scopeTexts.push(["", oauthScopeText[possibleScope]]);
|
else scopeTexts.push(["", oauthScopeText[possibleScope]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return scopeTexts;
|
return scopeTexts;
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -98,12 +98,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
import type { APIInstance } from "~types/entities/instance";
|
import type { APIInstance } from "~types/entities/instance";
|
||||||
import LoginInput from "../../components/LoginInput.vue"
|
import LoginInput from "../../components/LoginInput.vue";
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
const instanceInfo = await fetch("/api/v1/instance").then(res => res.json()) as APIInstance & {
|
const instanceInfo = (await fetch("/api/v1/instance").then((res) =>
|
||||||
tos_url: string
|
res.json(),
|
||||||
|
)) as APIInstance & {
|
||||||
|
tos_url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = ref<{
|
const errors = ref<{
|
||||||
|
|
@ -124,26 +126,40 @@ const registerUser = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append("email", (e.target as any).email.value);
|
const target = e.target as unknown as Record<string, HTMLInputElement>;
|
||||||
formData.append("password", (e.target as any).password.value);
|
|
||||||
formData.append("username", (e.target as any).username.value);
|
formData.append("email", target.email.value);
|
||||||
|
formData.append("password", target.password.value);
|
||||||
|
formData.append("username", target.username.value);
|
||||||
formData.append("reason", reason.value);
|
formData.append("reason", reason.value);
|
||||||
formData.append("locale", "en")
|
formData.append("locale", "en");
|
||||||
formData.append("agreement", "true");
|
formData.append("agreement", "true");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
fetch("/api/v1/accounts", {
|
fetch("/api/v1/accounts", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
}).then(async res => {
|
|
||||||
if (res.status === 422) {
|
|
||||||
errors.value = (await res.json() as any).details;
|
|
||||||
console.log(errors.value)
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
window.location.href = "/register/success";
|
|
||||||
}
|
|
||||||
}).catch(async err => {
|
|
||||||
console.error(err);
|
|
||||||
})
|
})
|
||||||
}
|
.then(async (res) => {
|
||||||
|
if (res.status === 422) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -6,9 +6,9 @@ import registerIndexVue from "./pages/register/index.vue";
|
||||||
import successVue from "./pages/register/success.vue";
|
import successVue from "./pages/register/success.vue";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ path: "/", component: indexVue },
|
{ path: "/", component: indexVue },
|
||||||
{ path: "/oauth/authorize", component: authorizeVue },
|
{ path: "/oauth/authorize", component: authorizeVue },
|
||||||
{ path: "/oauth/redirect", component: redirectVue },
|
{ path: "/oauth/redirect", component: redirectVue },
|
||||||
{ path: "/register", component: registerIndexVue },
|
{ path: "/register", component: registerIndexVue },
|
||||||
{ path: "/register/success", component: successVue },
|
{ path: "/register/success", component: successVue },
|
||||||
] as RouteRecordRaw[];
|
] as RouteRecordRaw[];
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
import { defineConfig } from "vite";
|
|
||||||
import UnoCSS from "unocss/vite";
|
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import UnoCSS from "unocss/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
import pkg from "../package.json";
|
import pkg from "../package.json";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "/",
|
base: "/",
|
||||||
build: {
|
build: {
|
||||||
outDir: "./dist",
|
outDir: "./dist",
|
||||||
},
|
},
|
||||||
// main.ts is in pages/ directory
|
// main.ts is in pages/ directory
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
vue: "vue/dist/vue.esm-bundler",
|
vue: "vue/dist/vue.esm-bundler",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
hmr: {
|
hmr: {
|
||||||
clientPort: 5173,
|
clientPort: 5173,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
__VERSION__: JSON.stringify(pkg.version),
|
__VERSION__: JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
ssr: {
|
ssr: {
|
||||||
noExternal: ["@prisma/client"],
|
noExternal: ["@prisma/client"],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
UnoCSS({
|
UnoCSS({
|
||||||
mode: "global",
|
mode: "global",
|
||||||
}),
|
}),
|
||||||
vue(),
|
vue(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import type { Server } from "./types";
|
||||||
import { HookTypes } from "./types";
|
import { HookTypes } from "./types";
|
||||||
|
|
||||||
const registerPlugin = (server: Server) => {
|
const registerPlugin = (server: Server) => {
|
||||||
server.on(HookTypes.OnPostCreate, (req, newPost, author) => {
|
server.on(HookTypes.OnPostCreate, (req, newPost, author) => {
|
||||||
console.log("New post created!");
|
console.log("New post created!");
|
||||||
console.log(`Post details: ${newPost.content} (${newPost.id})`);
|
console.log(`Post details: ${newPost.content} (${newPost.id})`);
|
||||||
console.log(`Made by ${author.username} (${author.id})`);
|
console.log(`Made by ${author.username} (${author.id})`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default registerPlugin;
|
export default registerPlugin;
|
||||||
|
|
|
||||||
280
plugins/types.ts
280
plugins/types.ts
|
|
@ -4,152 +4,152 @@ import type { UserWithRelations } from "~database/entities/User";
|
||||||
import type { LysandObjectType } from "~types/lysand/Object";
|
import type { LysandObjectType } from "~types/lysand/Object";
|
||||||
|
|
||||||
export enum HookTypes {
|
export enum HookTypes {
|
||||||
/**
|
/**
|
||||||
* Called before the server starts listening
|
* Called before the server starts listening
|
||||||
*/
|
*/
|
||||||
PreServe = "preServe",
|
PreServe = "preServe",
|
||||||
/**
|
/**
|
||||||
* Called after the server stops listening
|
* Called after the server stops listening
|
||||||
*/
|
*/
|
||||||
PostServe = "postServe",
|
PostServe = "postServe",
|
||||||
/**
|
/**
|
||||||
* Called on every HTTP request (before anything else is done)
|
* Called on every HTTP request (before anything else is done)
|
||||||
*/
|
*/
|
||||||
OnRequestReceive = "onRequestReceive",
|
OnRequestReceive = "onRequestReceive",
|
||||||
/**
|
/**
|
||||||
* Called on every HTTP request (after it is processed)
|
* Called on every HTTP request (after it is processed)
|
||||||
*/
|
*/
|
||||||
OnRequestProcessed = "onRequestProcessed",
|
OnRequestProcessed = "onRequestProcessed",
|
||||||
/**
|
/**
|
||||||
* Called on every object received (before it is parsed and added to the database)
|
* Called on every object received (before it is parsed and added to the database)
|
||||||
*/
|
*/
|
||||||
OnObjectReceive = "onObjectReceive",
|
OnObjectReceive = "onObjectReceive",
|
||||||
/**
|
/**
|
||||||
* Called on every object processed (after it is parsed and added to the database)
|
* Called on every object processed (after it is parsed and added to the database)
|
||||||
*/
|
*/
|
||||||
OnObjectProcessed = "onObjectProcessed",
|
OnObjectProcessed = "onObjectProcessed",
|
||||||
/**
|
/**
|
||||||
* Called when signature verification fails on an object
|
* Called when signature verification fails on an object
|
||||||
*/
|
*/
|
||||||
OnCryptoFail = "onCryptoFail",
|
OnCryptoFail = "onCryptoFail",
|
||||||
/**
|
/**
|
||||||
* Called when signature verification succeeds on an object
|
* Called when signature verification succeeds on an object
|
||||||
*/
|
*/
|
||||||
OnCryptoSuccess = "onCryptoSuccess",
|
OnCryptoSuccess = "onCryptoSuccess",
|
||||||
/**
|
/**
|
||||||
* Called when a user is banned by another user
|
* Called when a user is banned by another user
|
||||||
*/
|
*/
|
||||||
OnBan = "onBan",
|
OnBan = "onBan",
|
||||||
/**
|
/**
|
||||||
* Called when a user is suspended by another user
|
* Called when a user is suspended by another user
|
||||||
*/
|
*/
|
||||||
OnSuspend = "onSuspend",
|
OnSuspend = "onSuspend",
|
||||||
/**
|
/**
|
||||||
* Called when a user is blocked by another user
|
* Called when a user is blocked by another user
|
||||||
*/
|
*/
|
||||||
OnUserBlock = "onUserBlock",
|
OnUserBlock = "onUserBlock",
|
||||||
/**
|
/**
|
||||||
* Called when a user is muted by another user
|
* Called when a user is muted by another user
|
||||||
*/
|
*/
|
||||||
OnUserMute = "onUserMute",
|
OnUserMute = "onUserMute",
|
||||||
/**
|
/**
|
||||||
* Called when a user is followed by another user
|
* Called when a user is followed by another user
|
||||||
*/
|
*/
|
||||||
OnUserFollow = "onUserFollow",
|
OnUserFollow = "onUserFollow",
|
||||||
/**
|
/**
|
||||||
* Called when a user registers (before completing email verification)
|
* Called when a user registers (before completing email verification)
|
||||||
*/
|
*/
|
||||||
OnRegister = "onRegister",
|
OnRegister = "onRegister",
|
||||||
/**
|
/**
|
||||||
* Called when a user finishes registering (after completing email verification)
|
* Called when a user finishes registering (after completing email verification)
|
||||||
*/
|
*/
|
||||||
OnRegisterFinish = "onRegisterFinish",
|
OnRegisterFinish = "onRegisterFinish",
|
||||||
/**
|
/**
|
||||||
* Called when a user deletes their account
|
* Called when a user deletes their account
|
||||||
*/
|
*/
|
||||||
OnDeleteAccount = "onDeleteAccount",
|
OnDeleteAccount = "onDeleteAccount",
|
||||||
/**
|
/**
|
||||||
* Called when a post is created
|
* Called when a post is created
|
||||||
*/
|
*/
|
||||||
OnPostCreate = "onPostCreate",
|
OnPostCreate = "onPostCreate",
|
||||||
/**
|
/**
|
||||||
* Called when a post is deleted
|
* Called when a post is deleted
|
||||||
*/
|
*/
|
||||||
OnPostDelete = "onPostDelete",
|
OnPostDelete = "onPostDelete",
|
||||||
/**
|
/**
|
||||||
* Called when a post is updated
|
* Called when a post is updated
|
||||||
*/
|
*/
|
||||||
OnPostUpdate = "onPostUpdate",
|
OnPostUpdate = "onPostUpdate",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerStats {
|
export interface ServerStats {
|
||||||
postCount: number;
|
postCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerEvents {
|
interface ServerEvents {
|
||||||
[HookTypes.PreServe]: () => void;
|
[HookTypes.PreServe]: () => void;
|
||||||
[HookTypes.PostServe]: (stats: ServerStats) => void;
|
[HookTypes.PostServe]: (stats: ServerStats) => void;
|
||||||
[HookTypes.OnRequestReceive]: (req: Request) => void;
|
[HookTypes.OnRequestReceive]: (req: Request) => void;
|
||||||
[HookTypes.OnRequestProcessed]: (req: Request) => void;
|
[HookTypes.OnRequestProcessed]: (req: Request) => void;
|
||||||
[HookTypes.OnObjectReceive]: (obj: LysandObjectType) => void;
|
[HookTypes.OnObjectReceive]: (obj: LysandObjectType) => void;
|
||||||
[HookTypes.OnObjectProcessed]: (obj: LysandObjectType) => void;
|
[HookTypes.OnObjectProcessed]: (obj: LysandObjectType) => void;
|
||||||
[HookTypes.OnCryptoFail]: (
|
[HookTypes.OnCryptoFail]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
obj: LysandObjectType,
|
obj: LysandObjectType,
|
||||||
author: UserWithRelations,
|
author: UserWithRelations,
|
||||||
publicKey: string
|
publicKey: string,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnCryptoSuccess]: (
|
[HookTypes.OnCryptoSuccess]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
obj: LysandObjectType,
|
obj: LysandObjectType,
|
||||||
author: UserWithRelations,
|
author: UserWithRelations,
|
||||||
publicKey: string
|
publicKey: string,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnBan]: (
|
[HookTypes.OnBan]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
bannedUser: UserWithRelations,
|
bannedUser: UserWithRelations,
|
||||||
banner: UserWithRelations
|
banner: UserWithRelations,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnSuspend]: (
|
[HookTypes.OnSuspend]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
suspendedUser: UserWithRelations,
|
suspendedUser: UserWithRelations,
|
||||||
suspender: UserWithRelations
|
suspender: UserWithRelations,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnUserBlock]: (
|
[HookTypes.OnUserBlock]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
blockedUser: UserWithRelations,
|
blockedUser: UserWithRelations,
|
||||||
blocker: UserWithRelations
|
blocker: UserWithRelations,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnUserMute]: (
|
[HookTypes.OnUserMute]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
mutedUser: UserWithRelations,
|
mutedUser: UserWithRelations,
|
||||||
muter: UserWithRelations
|
muter: UserWithRelations,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnUserFollow]: (
|
[HookTypes.OnUserFollow]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
followedUser: UserWithRelations,
|
followedUser: UserWithRelations,
|
||||||
follower: UserWithRelations
|
follower: UserWithRelations,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnRegister]: (req: Request, newUser: UserWithRelations) => void;
|
[HookTypes.OnRegister]: (req: Request, newUser: UserWithRelations) => void;
|
||||||
[HookTypes.OnDeleteAccount]: (
|
[HookTypes.OnDeleteAccount]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
deletedUser: UserWithRelations
|
deletedUser: UserWithRelations,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnPostCreate]: (
|
[HookTypes.OnPostCreate]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
newPost: StatusWithRelations,
|
newPost: StatusWithRelations,
|
||||||
author: UserWithRelations
|
author: UserWithRelations,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnPostDelete]: (
|
[HookTypes.OnPostDelete]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
deletedPost: StatusWithRelations,
|
deletedPost: StatusWithRelations,
|
||||||
deleter: UserWithRelations
|
deleter: UserWithRelations,
|
||||||
) => void;
|
) => void;
|
||||||
[HookTypes.OnPostUpdate]: (
|
[HookTypes.OnPostUpdate]: (
|
||||||
req: Request,
|
req: Request,
|
||||||
updatedPost: StatusWithRelations,
|
updatedPost: StatusWithRelations,
|
||||||
updater: UserWithRelations
|
updater: UserWithRelations,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Server extends EventEmitter<ServerEvents> {}
|
export class Server extends EventEmitter<ServerEvents> {}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { config } from "config-manager";
|
||||||
// Proxies all `bunx prisma` commands with an environment variable
|
// Proxies all `bunx prisma` commands with an environment variable
|
||||||
|
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}\n`
|
`postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}\n`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ends
|
// Ends
|
||||||
|
|
|
||||||
188
routes.ts
188
routes.ts
|
|
@ -5,106 +5,106 @@ import type { APIRouteMeta } from "./types/api";
|
||||||
// This is to allow for compilation of the routes, so that we can minify them and
|
// This is to allow for compilation of the routes, so that we can minify them and
|
||||||
// node_modules in production
|
// node_modules in production
|
||||||
export const rawRoutes = {
|
export const rawRoutes = {
|
||||||
"/api/v1/accounts": "./server/api/api/v1/accounts",
|
"/api/v1/accounts": "./server/api/api/v1/accounts",
|
||||||
"/api/v1/accounts/familiar_followers":
|
"/api/v1/accounts/familiar_followers":
|
||||||
"+api/v1/accounts/familiar_followers/index",
|
"+api/v1/accounts/familiar_followers/index",
|
||||||
"/api/v1/accounts/relationships":
|
"/api/v1/accounts/relationships":
|
||||||
"./server/api/api/v1/accounts/relationships/index",
|
"./server/api/api/v1/accounts/relationships/index",
|
||||||
"/api/v1/accounts/search": "./server/api/api/v1/accounts/search/index",
|
"/api/v1/accounts/search": "./server/api/api/v1/accounts/search/index",
|
||||||
"/api/v1/accounts/update_credentials":
|
"/api/v1/accounts/update_credentials":
|
||||||
"./server/api/api/v1/accounts/update_credentials/index",
|
"./server/api/api/v1/accounts/update_credentials/index",
|
||||||
"/api/v1/accounts/verify_credentials":
|
"/api/v1/accounts/verify_credentials":
|
||||||
"./server/api/api/v1/accounts/verify_credentials/index",
|
"./server/api/api/v1/accounts/verify_credentials/index",
|
||||||
"/api/v1/apps": "./server/api/api/v1/apps/index",
|
"/api/v1/apps": "./server/api/api/v1/apps/index",
|
||||||
"/api/v1/apps/verify_credentials":
|
"/api/v1/apps/verify_credentials":
|
||||||
"./server/api/api/v1/apps/verify_credentials/index",
|
"./server/api/api/v1/apps/verify_credentials/index",
|
||||||
"/api/v1/blocks": "./server/api/api/v1/blocks/index",
|
"/api/v1/blocks": "./server/api/api/v1/blocks/index",
|
||||||
"/api/v1/custom_emojis": "./server/api/api/v1/custom_emojis/index",
|
"/api/v1/custom_emojis": "./server/api/api/v1/custom_emojis/index",
|
||||||
"/api/v1/favourites": "./server/api/api/v1/favourites/index",
|
"/api/v1/favourites": "./server/api/api/v1/favourites/index",
|
||||||
"/api/v1/follow_requests": "./server/api/api/v1/follow_requests/index",
|
"/api/v1/follow_requests": "./server/api/api/v1/follow_requests/index",
|
||||||
"/api/v1/instance": "./server/api/api/v1/instance/index",
|
"/api/v1/instance": "./server/api/api/v1/instance/index",
|
||||||
"/api/v1/media": "./server/api/api/v1/media/index",
|
"/api/v1/media": "./server/api/api/v1/media/index",
|
||||||
"/api/v1/mutes": "./server/api/api/v1/mutes/index",
|
"/api/v1/mutes": "./server/api/api/v1/mutes/index",
|
||||||
"/api/v1/notifications": "./server/api/api/v1/notifications/index",
|
"/api/v1/notifications": "./server/api/api/v1/notifications/index",
|
||||||
"/api/v1/profile/avatar": "./server/api/api/v1/profile/avatar",
|
"/api/v1/profile/avatar": "./server/api/api/v1/profile/avatar",
|
||||||
"/api/v1/profile/header": "./server/api/api/v1/profile/header",
|
"/api/v1/profile/header": "./server/api/api/v1/profile/header",
|
||||||
"/api/v1/statuses": "./server/api/api/v1/statuses/index",
|
"/api/v1/statuses": "./server/api/api/v1/statuses/index",
|
||||||
"/api/v1/timelines/home": "./server/api/api/v1/timelines/home",
|
"/api/v1/timelines/home": "./server/api/api/v1/timelines/home",
|
||||||
"/api/v1/timelines/public": "./server/api/api/v1/timelines/public",
|
"/api/v1/timelines/public": "./server/api/api/v1/timelines/public",
|
||||||
"/api/v2/media": "./server/api/api/v2/media/index",
|
"/api/v2/media": "./server/api/api/v2/media/index",
|
||||||
"/api/v2/search": "./server/api/api/v2/search/index",
|
"/api/v2/search": "./server/api/api/v2/search/index",
|
||||||
"/auth/login": "./server/api/auth/login/index",
|
"/auth/login": "./server/api/auth/login/index",
|
||||||
"/auth/redirect": "./server/api/auth/redirect/index",
|
"/auth/redirect": "./server/api/auth/redirect/index",
|
||||||
"/nodeinfo/2.0": "./server/api/nodeinfo/2.0/index",
|
"/nodeinfo/2.0": "./server/api/nodeinfo/2.0/index",
|
||||||
"/oauth/authorize-external": "./server/api/oauth/authorize-external/index",
|
"/oauth/authorize-external": "./server/api/oauth/authorize-external/index",
|
||||||
"/oauth/providers": "./server/api/oauth/providers/index",
|
"/oauth/providers": "./server/api/oauth/providers/index",
|
||||||
"/oauth/token": "./server/api/oauth/token/index",
|
"/oauth/token": "./server/api/oauth/token/index",
|
||||||
"/api/v1/accounts/[id]": "./server/api/api/v1/accounts/[id]/index",
|
"/api/v1/accounts/[id]": "./server/api/api/v1/accounts/[id]/index",
|
||||||
"/api/v1/accounts/[id]/block": "./server/api/api/v1/accounts/[id]/block",
|
"/api/v1/accounts/[id]/block": "./server/api/api/v1/accounts/[id]/block",
|
||||||
"/api/v1/accounts/[id]/follow": "./server/api/api/v1/accounts/[id]/follow",
|
"/api/v1/accounts/[id]/follow": "./server/api/api/v1/accounts/[id]/follow",
|
||||||
"/api/v1/accounts/[id]/followers":
|
"/api/v1/accounts/[id]/followers":
|
||||||
"./server/api/api/v1/accounts/[id]/followers",
|
"./server/api/api/v1/accounts/[id]/followers",
|
||||||
"/api/v1/accounts/[id]/following":
|
"/api/v1/accounts/[id]/following":
|
||||||
"./server/api/api/v1/accounts/[id]/following",
|
"./server/api/api/v1/accounts/[id]/following",
|
||||||
"/api/v1/accounts/[id]/mute": "./server/api/api/v1/accounts/[id]/mute",
|
"/api/v1/accounts/[id]/mute": "./server/api/api/v1/accounts/[id]/mute",
|
||||||
"/api/v1/accounts/[id]/note": "./server/api/api/v1/accounts/[id]/note",
|
"/api/v1/accounts/[id]/note": "./server/api/api/v1/accounts/[id]/note",
|
||||||
"/api/v1/accounts/[id]/pin": "./server/api/api/v1/accounts/[id]/pin",
|
"/api/v1/accounts/[id]/pin": "./server/api/api/v1/accounts/[id]/pin",
|
||||||
"/api/v1/accounts/[id]/remove_from_followers":
|
"/api/v1/accounts/[id]/remove_from_followers":
|
||||||
"./server/api/api/v1/accounts/[id]/remove_from_followers",
|
"./server/api/api/v1/accounts/[id]/remove_from_followers",
|
||||||
"/api/v1/accounts/[id]/statuses":
|
"/api/v1/accounts/[id]/statuses":
|
||||||
"./server/api/api/v1/accounts/[id]/statuses",
|
"./server/api/api/v1/accounts/[id]/statuses",
|
||||||
"/api/v1/accounts/[id]/unblock":
|
"/api/v1/accounts/[id]/unblock":
|
||||||
"./server/api/api/v1/accounts/[id]/unblock",
|
"./server/api/api/v1/accounts/[id]/unblock",
|
||||||
"/api/v1/accounts/[id]/unfollow":
|
"/api/v1/accounts/[id]/unfollow":
|
||||||
"./server/api/api/v1/accounts/[id]/unfollow",
|
"./server/api/api/v1/accounts/[id]/unfollow",
|
||||||
"/api/v1/accounts/[id]/unmute": "./server/api/api/v1/accounts/[id]/unmute",
|
"/api/v1/accounts/[id]/unmute": "./server/api/api/v1/accounts/[id]/unmute",
|
||||||
"/api/v1/accounts/[id]/unpin": "./server/api/api/v1/accounts/[id]/unpin",
|
"/api/v1/accounts/[id]/unpin": "./server/api/api/v1/accounts/[id]/unpin",
|
||||||
"/api/v1/follow_requests/[account_id]/authorize":
|
"/api/v1/follow_requests/[account_id]/authorize":
|
||||||
"./server/api/api/v1/follow_requests/[account_id]/authorize",
|
"./server/api/api/v1/follow_requests/[account_id]/authorize",
|
||||||
"/api/v1/follow_requests/[account_id]/reject":
|
"/api/v1/follow_requests/[account_id]/reject":
|
||||||
"./server/api/api/v1/follow_requests/[account_id]/reject",
|
"./server/api/api/v1/follow_requests/[account_id]/reject",
|
||||||
"/api/v1/media/[id]": "./server/api/api/v1/media/[id]/index",
|
"/api/v1/media/[id]": "./server/api/api/v1/media/[id]/index",
|
||||||
"/api/v1/statuses/[id]": "./server/api/api/v1/statuses/[id]/index",
|
"/api/v1/statuses/[id]": "./server/api/api/v1/statuses/[id]/index",
|
||||||
"/api/v1/statuses/[id]/context":
|
"/api/v1/statuses/[id]/context":
|
||||||
"./server/api/api/v1/statuses/[id]/context",
|
"./server/api/api/v1/statuses/[id]/context",
|
||||||
"/api/v1/statuses/[id]/favourite":
|
"/api/v1/statuses/[id]/favourite":
|
||||||
"./server/api/api/v1/statuses/[id]/favourite",
|
"./server/api/api/v1/statuses/[id]/favourite",
|
||||||
"/api/v1/statuses/[id]/favourited_by":
|
"/api/v1/statuses/[id]/favourited_by":
|
||||||
"./server/api/api/v1/statuses/[id]/favourited_by",
|
"./server/api/api/v1/statuses/[id]/favourited_by",
|
||||||
"/api/v1/statuses/[id]/pin": "./server/api/api/v1/statuses/[id]/pin",
|
"/api/v1/statuses/[id]/pin": "./server/api/api/v1/statuses/[id]/pin",
|
||||||
"/api/v1/statuses/[id]/reblog": "./server/api/api/v1/statuses/[id]/reblog",
|
"/api/v1/statuses/[id]/reblog": "./server/api/api/v1/statuses/[id]/reblog",
|
||||||
"/api/v1/statuses/[id]/reblogged_by":
|
"/api/v1/statuses/[id]/reblogged_by":
|
||||||
"./server/api/api/v1/statuses/[id]/reblogged_by",
|
"./server/api/api/v1/statuses/[id]/reblogged_by",
|
||||||
"/api/v1/statuses/[id]/source": "./server/api/api/v1/statuses/[id]/source",
|
"/api/v1/statuses/[id]/source": "./server/api/api/v1/statuses/[id]/source",
|
||||||
"/api/v1/statuses/[id]/unfavourite":
|
"/api/v1/statuses/[id]/unfavourite":
|
||||||
"./server/api/api/v1/statuses/[id]/unfavourite",
|
"./server/api/api/v1/statuses/[id]/unfavourite",
|
||||||
"/api/v1/statuses/[id]/unpin": "./server/api/api/v1/statuses/[id]/unpin",
|
"/api/v1/statuses/[id]/unpin": "./server/api/api/v1/statuses/[id]/unpin",
|
||||||
"/api/v1/statuses/[id]/unreblog":
|
"/api/v1/statuses/[id]/unreblog":
|
||||||
"./server/api/api/v1/statuses/[id]/unreblog",
|
"./server/api/api/v1/statuses/[id]/unreblog",
|
||||||
"/media/[id]": "./server/api/media/[id]/index",
|
"/media/[id]": "./server/api/media/[id]/index",
|
||||||
"/oauth/callback/[issuer]": "./server/api/oauth/callback/[issuer]/index",
|
"/oauth/callback/[issuer]": "./server/api/oauth/callback/[issuer]/index",
|
||||||
"/object/[uuid]": "./server/api/object/[uuid]/index",
|
"/object/[uuid]": "./server/api/object/[uuid]/index",
|
||||||
"/users/[uuid]": "./server/api/users/[uuid]/index",
|
"/users/[uuid]": "./server/api/users/[uuid]/index",
|
||||||
"/users/[uuid]/inbox": "./server/api/users/[uuid]/inbox/index",
|
"/users/[uuid]/inbox": "./server/api/users/[uuid]/inbox/index",
|
||||||
"/users/[uuid]/outbox": "./server/api/users/[uuid]/outbox/index",
|
"/users/[uuid]/outbox": "./server/api/users/[uuid]/outbox/index",
|
||||||
"/[...404]": "./server/api/[...404]",
|
"/[...404]": "./server/api/[...404]",
|
||||||
} as Record<string, string>;
|
} as Record<string, string>;
|
||||||
|
|
||||||
// Returns the route filesystem path when given a URL
|
// Returns the route filesystem path when given a URL
|
||||||
export const routeMatcher = new Bun.FileSystemRouter({
|
export const routeMatcher = new Bun.FileSystemRouter({
|
||||||
style: "nextjs",
|
style: "nextjs",
|
||||||
dir: process.cwd() + "/server/api",
|
dir: `${process.cwd()}/server/api`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const matchRoute = async <T = Record<string, never>>(url: string) => {
|
export const matchRoute = async <T = Record<string, never>>(url: string) => {
|
||||||
const route = routeMatcher.match(url);
|
const route = routeMatcher.match(url);
|
||||||
if (!route) return { file: null, matchedRoute: null };
|
if (!route) return { file: null, matchedRoute: null };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
file: (await import(rawRoutes[route.name])) as {
|
file: (await import(rawRoutes[route.name])) as {
|
||||||
default: RouteHandler<T>;
|
default: RouteHandler<T>;
|
||||||
meta: APIRouteMeta;
|
meta: APIRouteMeta;
|
||||||
},
|
},
|
||||||
matchedRoute: route,
|
matchedRoute: route,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
427
server.ts
427
server.ts
|
|
@ -1,251 +1,246 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import type { Config } from "config-manager";
|
||||||
import { matches } from "ip-matching";
|
import { matches } from "ip-matching";
|
||||||
import { getFromRequest } from "~database/entities/User";
|
|
||||||
import { type Config } from "config-manager";
|
|
||||||
import type { LogManager, MultiLogManager } from "log-manager";
|
import type { LogManager, MultiLogManager } from "log-manager";
|
||||||
import { LogLevel } from "log-manager";
|
import { LogLevel } from "log-manager";
|
||||||
import { RequestParser } from "request-parser";
|
import { RequestParser } from "request-parser";
|
||||||
|
import { getFromRequest } from "~database/entities/User";
|
||||||
import { matchRoute } from "~routes";
|
import { matchRoute } from "~routes";
|
||||||
|
|
||||||
export const createServer = (
|
export const createServer = (
|
||||||
config: Config,
|
config: Config,
|
||||||
logger: LogManager | MultiLogManager,
|
logger: LogManager | MultiLogManager,
|
||||||
isProd: boolean
|
isProd: boolean,
|
||||||
) =>
|
) =>
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
port: config.http.bind_port,
|
port: config.http.bind_port,
|
||||||
hostname: config.http.bind || "0.0.0.0", // defaults to "0.0.0.0"
|
hostname: config.http.bind || "0.0.0.0", // defaults to "0.0.0.0"
|
||||||
async fetch(req) {
|
async fetch(req) {
|
||||||
// Check for banned IPs
|
// Check for banned IPs
|
||||||
const request_ip = this.requestIP(req)?.address ?? "";
|
const request_ip = this.requestIP(req)?.address ?? "";
|
||||||
|
|
||||||
for (const ip of config.http.banned_ips) {
|
for (const ip of config.http.banned_ips) {
|
||||||
try {
|
try {
|
||||||
if (matches(ip, request_ip)) {
|
if (matches(ip, request_ip)) {
|
||||||
return new Response(undefined, {
|
return new Response(undefined, {
|
||||||
status: 403,
|
status: 403,
|
||||||
statusText: "Forbidden",
|
statusText: "Forbidden",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[-] Error while parsing banned IP "${ip}" `);
|
console.error(`[-] Error while parsing banned IP "${ip}" `);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for banned user agents (regex)
|
// Check for banned user agents (regex)
|
||||||
const ua = req.headers.get("User-Agent") ?? "";
|
const ua = req.headers.get("User-Agent") ?? "";
|
||||||
|
|
||||||
for (const agent of config.http.banned_user_agents) {
|
for (const agent of config.http.banned_user_agents) {
|
||||||
if (new RegExp(agent).test(ua)) {
|
if (new RegExp(agent).test(ua)) {
|
||||||
return new Response(undefined, {
|
return new Response(undefined, {
|
||||||
status: 403,
|
status: 403,
|
||||||
statusText: "Forbidden",
|
statusText: "Forbidden",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.http.bait.enabled) {
|
if (config.http.bait.enabled) {
|
||||||
// Check for bait IPs
|
// Check for bait IPs
|
||||||
for (const ip of config.http.bait.bait_ips) {
|
for (const ip of config.http.bait.bait_ips) {
|
||||||
try {
|
try {
|
||||||
if (matches(ip, request_ip)) {
|
if (matches(ip, request_ip)) {
|
||||||
const file = Bun.file(
|
const file = Bun.file(
|
||||||
config.http.bait.send_file ||
|
config.http.bait.send_file ||
|
||||||
"./pages/beemovie.txt"
|
"./pages/beemovie.txt",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
return new Response(file);
|
return new Response(file);
|
||||||
} else {
|
}
|
||||||
await logger.log(
|
await logger.log(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Server.Bait",
|
"Server.Bait",
|
||||||
`Bait file not found: ${config.http.bait.send_file}`
|
`Bait file not found: ${config.http.bait.send_file}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
} catch (e) {
|
console.error(
|
||||||
console.error(
|
`[-] Error while parsing bait IP "${ip}" `,
|
||||||
`[-] Error while parsing bait IP "${ip}" `
|
);
|
||||||
);
|
throw e;
|
||||||
throw e;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check for bait user agents (regex)
|
// Check for bait user agents (regex)
|
||||||
for (const agent of config.http.bait.bait_user_agents) {
|
for (const agent of config.http.bait.bait_user_agents) {
|
||||||
console.log(agent);
|
console.log(agent);
|
||||||
if (new RegExp(agent).test(ua)) {
|
if (new RegExp(agent).test(ua)) {
|
||||||
const file = Bun.file(
|
const file = Bun.file(
|
||||||
config.http.bait.send_file || "./pages/beemovie.txt"
|
config.http.bait.send_file ||
|
||||||
);
|
"./pages/beemovie.txt",
|
||||||
|
);
|
||||||
|
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
return new Response(file);
|
return new Response(file);
|
||||||
} else {
|
}
|
||||||
await logger.log(
|
await logger.log(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Server.Bait",
|
"Server.Bait",
|
||||||
`Bait file not found: ${config.http.bait.send_file}`
|
`Bait file not found: ${config.http.bait.send_file}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (config.logging.log_requests) {
|
if (config.logging.log_requests) {
|
||||||
await logger.logRequest(
|
await logger.logRequest(
|
||||||
req,
|
req,
|
||||||
config.logging.log_ip ? request_ip : undefined,
|
config.logging.log_ip ? request_ip : undefined,
|
||||||
config.logging.log_requests_verbose
|
config.logging.log_requests_verbose,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
if (req.method === "OPTIONS") {
|
||||||
return jsonResponse({});
|
return jsonResponse({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { file: filePromise, matchedRoute } = await matchRoute(
|
const { file: filePromise, matchedRoute } = await matchRoute(
|
||||||
req.url
|
req.url,
|
||||||
);
|
);
|
||||||
|
|
||||||
const file = filePromise;
|
const file = filePromise;
|
||||||
|
|
||||||
if (matchedRoute && file == undefined) {
|
if (matchedRoute && file === undefined) {
|
||||||
await logger.log(
|
await logger.log(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Server",
|
"Server",
|
||||||
`Route file ${matchedRoute.filePath} not found or not registered in the routes file`
|
`Route file ${matchedRoute.filePath} not found or not registered in the routes file`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return errorResponse("Route not found", 500);
|
return errorResponse("Route not found", 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (matchedRoute && matchedRoute.name !== "/[...404]" && file) {
|
||||||
matchedRoute &&
|
const meta = file.meta;
|
||||||
matchedRoute.name !== "/[...404]" &&
|
|
||||||
file != undefined
|
|
||||||
) {
|
|
||||||
const meta = file.meta;
|
|
||||||
|
|
||||||
// Check for allowed requests
|
// Check for allowed requests
|
||||||
if (!meta.allowedMethods.includes(req.method as any)) {
|
// @ts-expect-error Stupid error
|
||||||
return new Response(undefined, {
|
if (!meta.allowedMethods.includes(req.method as string)) {
|
||||||
status: 405,
|
return new Response(undefined, {
|
||||||
statusText: `Method not allowed: allowed methods are: ${meta.allowedMethods.join(
|
status: 405,
|
||||||
", "
|
statusText: `Method not allowed: allowed methods are: ${meta.allowedMethods.join(
|
||||||
)}`,
|
", ",
|
||||||
});
|
)}`,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Check for ratelimits
|
// TODO: Check for ratelimits
|
||||||
const auth = await getFromRequest(req);
|
const auth = await getFromRequest(req);
|
||||||
|
|
||||||
// Check for authentication if required
|
// Check for authentication if required
|
||||||
if (meta.auth.required) {
|
if (meta.auth.required) {
|
||||||
if (!auth.user) {
|
if (!auth.user) {
|
||||||
return new Response(undefined, {
|
return new Response(undefined, {
|
||||||
status: 401,
|
status: 401,
|
||||||
statusText: "Unauthorized",
|
statusText: "Unauthorized",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
(meta.auth.requiredOnMethods ?? []).includes(
|
// @ts-expect-error Stupid error
|
||||||
req.method as any
|
(meta.auth.requiredOnMethods ?? []).includes(req.method)
|
||||||
)
|
) {
|
||||||
) {
|
if (!auth.user) {
|
||||||
if (!auth.user) {
|
return new Response(undefined, {
|
||||||
return new Response(undefined, {
|
status: 401,
|
||||||
status: 401,
|
statusText: "Unauthorized",
|
||||||
statusText: "Unauthorized",
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let parsedRequest = {};
|
let parsedRequest = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsedRequest = await new RequestParser(req).toObject();
|
parsedRequest = await new RequestParser(req).toObject();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.logError(
|
await logger.logError(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Server.RouteRequestParser",
|
"Server.RouteRequestParser",
|
||||||
e as Error
|
e as Error,
|
||||||
);
|
);
|
||||||
return new Response(undefined, {
|
return new Response(undefined, {
|
||||||
status: 400,
|
status: 400,
|
||||||
statusText: "Bad request",
|
statusText: "Bad request",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await file.default(req.clone(), matchedRoute, {
|
return await file.default(req.clone(), matchedRoute, {
|
||||||
auth,
|
auth,
|
||||||
parsedRequest,
|
parsedRequest,
|
||||||
// To avoid having to rewrite each route
|
// To avoid having to rewrite each route
|
||||||
configManager: {
|
configManager: {
|
||||||
getConfig: () => Promise.resolve(config),
|
getConfig: () => Promise.resolve(config),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (matchedRoute?.name === "/[...404]" || !matchedRoute) {
|
}
|
||||||
if (new URL(req.url).pathname.startsWith("/api")) {
|
if (matchedRoute?.name === "/[...404]" || !matchedRoute) {
|
||||||
return errorResponse("Route not found", 404);
|
if (new URL(req.url).pathname.startsWith("/api")) {
|
||||||
}
|
return errorResponse("Route not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
// Proxy response from Vite at localhost:5173 if in development mode
|
// Proxy response from Vite at localhost:5173 if in development mode
|
||||||
if (isProd) {
|
if (isProd) {
|
||||||
if (new URL(req.url).pathname.startsWith("/assets")) {
|
if (new URL(req.url).pathname.startsWith("/assets")) {
|
||||||
const file = Bun.file(
|
const file = Bun.file(
|
||||||
`./pages/dist${new URL(req.url).pathname}`
|
`./pages/dist${new URL(req.url).pathname}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Serve from pages/dist/assets
|
// Serve from pages/dist/assets
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
return new Response(file);
|
return new Response(file);
|
||||||
} else return errorResponse("Asset not found", 404);
|
}
|
||||||
}
|
return errorResponse("Asset not found", 404);
|
||||||
if (new URL(req.url).pathname.startsWith("/api")) {
|
}
|
||||||
return errorResponse("Route 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
|
// Serve from pages/dist
|
||||||
return new Response(file);
|
return new Response(file);
|
||||||
} else {
|
}
|
||||||
const proxy = await fetch(
|
const proxy = await fetch(
|
||||||
req.url.replace(
|
req.url.replace(
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
"http://localhost:5173"
|
"http://localhost:5173",
|
||||||
)
|
),
|
||||||
).catch(async e => {
|
).catch(async (e) => {
|
||||||
await logger.logError(
|
await logger.logError(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Server.Proxy",
|
"Server.Proxy",
|
||||||
e as Error
|
e as Error,
|
||||||
);
|
);
|
||||||
await logger.log(
|
await logger.log(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Server.Proxy",
|
"Server.Proxy",
|
||||||
`The development Vite server is not running or the route is not found: ${req.url.replace(
|
`The development Vite server is not running or the route is not found: ${req.url.replace(
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
"http://localhost:5173"
|
"http://localhost:5173",
|
||||||
)}`
|
)}`,
|
||||||
);
|
);
|
||||||
return errorResponse("Route not found", 404);
|
return errorResponse("Route not found", 404);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
proxy.status !== 404 &&
|
proxy.status !== 404 &&
|
||||||
!(await proxy.clone().text()).includes("404 Not Found")
|
!(await proxy.clone().text()).includes("404 Not Found")
|
||||||
) {
|
) {
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorResponse("Route not found", 404);
|
return errorResponse("Route not found", 404);
|
||||||
}
|
}
|
||||||
} else {
|
return errorResponse("Route not found", 404);
|
||||||
return errorResponse("Route not found", 404);
|
},
|
||||||
}
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,22 @@
|
||||||
import { xmlResponse } from "@response";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { xmlResponse } from "@response";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
duration: 60,
|
duration: 60,
|
||||||
max: 60,
|
max: 60,
|
||||||
},
|
},
|
||||||
route: "/.well-known/host-meta",
|
route: "/.well-known/host-meta",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
|
|
||||||
return xmlResponse(`
|
return xmlResponse(`
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||||
<Link rel="lrdd" template="${config.http.base_url}/.well-known/webfinger?resource={uri}"/>
|
<Link rel="lrdd" template="${config.http.base_url}/.well-known/webfinger?resource={uri}"/>
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,49 @@
|
||||||
import { jsonResponse } from "@response";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { jsonResponse } from "@response";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
duration: 60,
|
duration: 60,
|
||||||
max: 60,
|
max: 60,
|
||||||
},
|
},
|
||||||
route: "/.well-known/lysand",
|
route: "/.well-known/lysand",
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
|
|
||||||
// In the format acct:name@example.com
|
// In the format acct:name@example.com
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
type: "ServerMetadata",
|
type: "ServerMetadata",
|
||||||
name: config.instance.name,
|
name: config.instance.name,
|
||||||
version: "0.0.1",
|
version: "0.0.1",
|
||||||
description: config.instance.description,
|
description: config.instance.description,
|
||||||
logo: config.instance.logo ? [
|
logo: config.instance.logo
|
||||||
{
|
? [
|
||||||
content: config.instance.logo,
|
{
|
||||||
content_type: `image/${config.instance.logo.split(".")[1]}`,
|
content: config.instance.logo,
|
||||||
}
|
content_type: `image/${
|
||||||
] : undefined,
|
config.instance.logo.split(".")[1]
|
||||||
banner: config.instance.banner ? [
|
}`,
|
||||||
{
|
},
|
||||||
content: config.instance.banner,
|
]
|
||||||
content_type: `image/${config.instance.banner.split(".")[1]}`,
|
: undefined,
|
||||||
}
|
banner: config.instance.banner
|
||||||
] : undefined,
|
? [
|
||||||
supported_extensions: [
|
{
|
||||||
"org.lysand:custom_emojis"
|
content: config.instance.banner,
|
||||||
],
|
content_type: `image/${
|
||||||
|
config.instance.banner.split(".")[1]
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined,
|
||||||
|
supported_extensions: ["org.lysand:custom_emojis"],
|
||||||
website: "https://lysand.org",
|
website: "https://lysand.org",
|
||||||
// TODO: Add admins, moderators field
|
// TODO: Add admins, moderators field
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,24 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
duration: 60,
|
duration: 60,
|
||||||
max: 60,
|
max: 60,
|
||||||
},
|
},
|
||||||
route: "/.well-known/nodeinfo",
|
route: "/.well-known/nodeinfo",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
|
|
||||||
return new Response("", {
|
return new Response("", {
|
||||||
status: 301,
|
status: 301,
|
||||||
headers: {
|
headers: {
|
||||||
Location: `${config.http.base_url}/.well-known/nodeinfo/2.0`,
|
Location: `${config.http.base_url}/.well-known/nodeinfo/2.0`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,59 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
duration: 60,
|
duration: 60,
|
||||||
max: 60,
|
max: 60,
|
||||||
},
|
},
|
||||||
route: "/.well-known/webfinger",
|
route: "/.well-known/webfinger",
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
// In the format acct:name@example.com
|
// In the format acct:name@example.com
|
||||||
const resource = matchedRoute.query.resource;
|
const resource = matchedRoute.query.resource;
|
||||||
const requestedUser = resource.split("acct:")[1];
|
const requestedUser = resource.split("acct:")[1];
|
||||||
|
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
const host = new URL(config.http.base_url).hostname;
|
const host = new URL(config.http.base_url).hostname;
|
||||||
|
|
||||||
// Check if user is a local user
|
// Check if user is a local user
|
||||||
if (requestedUser.split("@")[1] !== host) {
|
if (requestedUser.split("@")[1] !== host) {
|
||||||
return errorResponse("User is a remote user", 404);
|
return errorResponse("User is a remote user", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { username: requestedUser.split("@")[0] },
|
where: { username: requestedUser.split("@")[0] },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return errorResponse("User not found", 404);
|
return errorResponse("User not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
subject: `acct:${user.username}@${host}`,
|
subject: `acct:${user.username}@${host}`,
|
||||||
|
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
rel: "self",
|
rel: "self",
|
||||||
type: "application/activity+json",
|
type: "application/activity+json",
|
||||||
href: `${config.http.base_url}/users/${user.username}/actor`
|
href: `${config.http.base_url}/users/${user.username}/actor`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: "https://webfinger.net/rel/profile-page",
|
rel: "https://webfinger.net/rel/profile-page",
|
||||||
type: "text/html",
|
type: "text/html",
|
||||||
href: `${config.http.base_url}/users/${user.username}`
|
href: `${config.http.base_url}/users/${user.username}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: "self",
|
rel: "self",
|
||||||
type: "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
type: 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
href: `${config.http.base_url}/users/${user.username}/actor`
|
href: `${config.http.base_url}/users/${user.username}/actor`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,20 @@ import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse } from "@response";
|
import { errorResponse } from "@response";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
allowedMethods: ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
duration: 60,
|
duration: 60,
|
||||||
max: 100,
|
max: 100,
|
||||||
},
|
},
|
||||||
route: "/[...404]",
|
route: "/[...404]",
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default catch-all route, returns a 404 error.
|
* Default catch-all route, returns a 404 error.
|
||||||
*/
|
*/
|
||||||
export default apiRoute(() => {
|
export default apiRoute(() => {
|
||||||
return errorResponse("This API route does not exist", 404);
|
return errorResponse("This API route does not exist", 404);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,81 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/block",
|
route: "/accounts/:id/block",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:blocks"],
|
oauthPermissions: ["write:blocks"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocks a user
|
* Blocks a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relationship.blocking) {
|
if (!relationship.blocking) {
|
||||||
relationship.blocking = true;
|
relationship.blocking = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
blocking: true,
|
blocking: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,99 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/follow",
|
route: "/accounts/:id/follow",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:follows"],
|
oauthPermissions: ["write:follows"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Follow a user
|
* Follow a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
reblogs?: boolean;
|
reblogs?: boolean;
|
||||||
notify?: boolean;
|
notify?: boolean;
|
||||||
languages?: string[];
|
languages?: string[];
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const { languages, notify, reblogs } = extraData.parsedRequest;
|
const { languages, notify, reblogs } = extraData.parsedRequest;
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relationship.following) {
|
if (!relationship.following) {
|
||||||
relationship.following = true;
|
relationship.following = true;
|
||||||
}
|
}
|
||||||
if (reblogs) {
|
if (reblogs) {
|
||||||
relationship.showingReblogs = true;
|
relationship.showingReblogs = true;
|
||||||
}
|
}
|
||||||
if (notify) {
|
if (notify) {
|
||||||
relationship.notifying = true;
|
relationship.notifying = true;
|
||||||
}
|
}
|
||||||
if (languages) {
|
if (languages) {
|
||||||
relationship.languages = languages;
|
relationship.languages = languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
following: true,
|
following: true,
|
||||||
showingReblogs: reblogs ?? false,
|
showingReblogs: reblogs ?? false,
|
||||||
notifying: notify ?? false,
|
notifying: notify ?? false,
|
||||||
languages: languages ?? [],
|
languages: languages ?? [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,82 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import { userToAPI } from "~database/entities/User";
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 60,
|
max: 60,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/followers",
|
route: "/accounts/:id/followers",
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
oauthPermissions: [],
|
oauthPermissions: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all statuses for a user
|
* Fetch all statuses for a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
max_id?: string;
|
max_id?: string;
|
||||||
since_id?: string;
|
since_id?: string;
|
||||||
min_id?: string;
|
min_id?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
// TODO: Add pinned
|
// TODO: Add pinned
|
||||||
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
|
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
|
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
const objects = await client.user.findMany({
|
const objects = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
relationships: {
|
relationships: {
|
||||||
some: {
|
some: {
|
||||||
subjectId: user.id,
|
subjectId: user.id,
|
||||||
following: true,
|
following: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
id: {
|
id: {
|
||||||
lt: max_id,
|
lt: max_id,
|
||||||
gt: min_id,
|
gt: min_id,
|
||||||
gte: since_id,
|
gte: since_id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
take: Number(limit),
|
take: Number(limit),
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: "desc",
|
id: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Constuct HTTP Link header (next and prev)
|
// Constuct HTTP Link header (next and prev)
|
||||||
const linkHeader = [];
|
const linkHeader = [];
|
||||||
if (objects.length > 0) {
|
if (objects.length > 0) {
|
||||||
const urlWithoutQuery = req.url.split("?")[0];
|
const urlWithoutQuery = req.url.split("?")[0];
|
||||||
linkHeader.push(
|
linkHeader.push(
|
||||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
await Promise.all(objects.map(object => userToAPI(object))),
|
await Promise.all(objects.map((object) => userToAPI(object))),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: linkHeader.join(", "),
|
Link: linkHeader.join(", "),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,82 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import { userToAPI } from "~database/entities/User";
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 60,
|
max: 60,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/following",
|
route: "/accounts/:id/following",
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
oauthPermissions: [],
|
oauthPermissions: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all statuses for a user
|
* Fetch all statuses for a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
max_id?: string;
|
max_id?: string;
|
||||||
since_id?: string;
|
since_id?: string;
|
||||||
min_id?: string;
|
min_id?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
// TODO: Add pinned
|
// TODO: Add pinned
|
||||||
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
|
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
|
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
const objects = await client.user.findMany({
|
const objects = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
relationshipSubjects: {
|
relationshipSubjects: {
|
||||||
some: {
|
some: {
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
following: true,
|
following: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
id: {
|
id: {
|
||||||
lt: max_id,
|
lt: max_id,
|
||||||
gt: min_id,
|
gt: min_id,
|
||||||
gte: since_id,
|
gte: since_id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
take: Number(limit),
|
take: Number(limit),
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: "desc",
|
id: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Constuct HTTP Link header (next and prev)
|
// Constuct HTTP Link header (next and prev)
|
||||||
const linkHeader = [];
|
const linkHeader = [];
|
||||||
if (objects.length > 0) {
|
if (objects.length > 0) {
|
||||||
const urlWithoutQuery = req.url.split("?")[0];
|
const urlWithoutQuery = req.url.split("?")[0];
|
||||||
linkHeader.push(
|
linkHeader.push(
|
||||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
await Promise.all(objects.map(object => userToAPI(object))),
|
await Promise.all(objects.map((object) => userToAPI(object))),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: linkHeader.join(", "),
|
Link: linkHeader.join(", "),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,46 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import type { UserWithRelations } from "~database/entities/User";
|
import type { UserWithRelations } from "~database/entities/User";
|
||||||
import { userToAPI } from "~database/entities/User";
|
import { userToAPI } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id",
|
route: "/accounts/:id",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: [],
|
oauthPermissions: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a user
|
* Fetch a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
// Check if ID is valid UUID
|
// Check if ID is valid UUID
|
||||||
if (!id.match(/^[0-9a-fA-F]{24}$/)) {
|
if (!id.match(/^[0-9a-fA-F]{24}$/)) {
|
||||||
return errorResponse("Invalid ID", 404);
|
return errorResponse("Invalid ID", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
let foundUser: UserWithRelations | null;
|
let foundUser: UserWithRelations | null;
|
||||||
try {
|
try {
|
||||||
foundUser = await client.user.findUnique({
|
foundUser = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return errorResponse("Invalid ID", 404);
|
return errorResponse("Invalid ID", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundUser) return errorResponse("User not found", 404);
|
if (!foundUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
return jsonResponse(userToAPI(foundUser, user?.id === foundUser.id));
|
return jsonResponse(userToAPI(foundUser, user?.id === foundUser.id));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,93 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/mute",
|
route: "/accounts/:id/mute",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:mutes"],
|
oauthPermissions: ["write:mutes"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mute a user
|
* Mute a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
notifications: boolean;
|
notifications: boolean;
|
||||||
duration: number;
|
duration: number;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { notifications, duration } = extraData.parsedRequest;
|
const { notifications, duration } = extraData.parsedRequest;
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relationship.muting) {
|
if (!relationship.muting) {
|
||||||
relationship.muting = true;
|
relationship.muting = true;
|
||||||
}
|
}
|
||||||
if (notifications ?? true) {
|
if (notifications ?? true) {
|
||||||
relationship.mutingNotifications = true;
|
relationship.mutingNotifications = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
muting: true,
|
muting: true,
|
||||||
mutingNotifications: notifications ?? true,
|
mutingNotifications: notifications ?? true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Implement duration
|
// TODO: Implement duration
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,83 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/note",
|
route: "/accounts/:id/note",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:accounts"],
|
oauthPermissions: ["write:accounts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a user note
|
* Sets a user note
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
comment: string;
|
comment: string;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const { comment } = extraData.parsedRequest;
|
const { comment } = extraData.parsedRequest;
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
relationship.note = comment ?? "";
|
relationship.note = comment ?? "";
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
note: relationship.note,
|
note: relationship.note,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,81 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/pin",
|
route: "/accounts/:id/pin",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:accounts"],
|
oauthPermissions: ["write:accounts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pin a user
|
* Pin a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relationship.endorsed) {
|
if (!relationship.endorsed) {
|
||||||
relationship.endorsed = true;
|
relationship.endorsed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
endorsed: true,
|
endorsed: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,95 +1,95 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/remove_from_followers",
|
route: "/accounts/:id/remove_from_followers",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:follows"],
|
oauthPermissions: ["write:follows"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes an account from your followers list
|
* Removes an account from your followers list
|
||||||
*/
|
*/
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relationship.followedBy) {
|
if (relationship.followedBy) {
|
||||||
relationship.followedBy = false;
|
relationship.followedBy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
followedBy: false,
|
followedBy: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user.instanceId === null) {
|
if (user.instanceId === null) {
|
||||||
// Also remove from followers list
|
// Also remove from followers list
|
||||||
await client.relationship.updateMany({
|
await client.relationship.updateMany({
|
||||||
where: {
|
where: {
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
subjectId: self.id,
|
subjectId: self.id,
|
||||||
following: true,
|
following: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
following: false,
|
following: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,134 +1,136 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { statusToAPI } from "~database/entities/Status";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import { statusToAPI } from "~database/entities/Status";
|
||||||
import {
|
import {
|
||||||
userRelations,
|
statusAndUserRelations,
|
||||||
statusAndUserRelations,
|
userRelations,
|
||||||
} from "~database/entities/relations";
|
} from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/statuses",
|
route: "/accounts/:id/statuses",
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
oauthPermissions: ["read:statuses"],
|
oauthPermissions: ["read:statuses"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all statuses for a user
|
* Fetch all statuses for a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
max_id?: string;
|
max_id?: string;
|
||||||
since_id?: string;
|
since_id?: string;
|
||||||
min_id?: string;
|
min_id?: string;
|
||||||
limit?: string;
|
limit?: string;
|
||||||
only_media?: boolean;
|
only_media?: boolean;
|
||||||
exclude_replies?: boolean;
|
exclude_replies?: boolean;
|
||||||
exclude_reblogs?: boolean;
|
exclude_reblogs?: boolean;
|
||||||
// TODO: Add with_muted
|
// TODO: Add with_muted
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
tagged?: string;
|
tagged?: string;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
// TODO: Add pinned
|
// TODO: Add pinned
|
||||||
const {
|
const {
|
||||||
max_id,
|
max_id,
|
||||||
min_id,
|
min_id,
|
||||||
since_id,
|
since_id,
|
||||||
limit = "20",
|
limit = "20",
|
||||||
exclude_reblogs,
|
exclude_reblogs,
|
||||||
pinned,
|
pinned,
|
||||||
} = extraData.parsedRequest;
|
} = extraData.parsedRequest;
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
if (pinned) {
|
if (pinned) {
|
||||||
const objects = await client.status.findMany({
|
const objects = await client.status.findMany({
|
||||||
where: {
|
where: {
|
||||||
authorId: id,
|
authorId: id,
|
||||||
isReblog: false,
|
isReblog: false,
|
||||||
pinnedBy: {
|
pinnedBy: {
|
||||||
some: {
|
some: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
id: {
|
id: {
|
||||||
lt: max_id,
|
lt: max_id,
|
||||||
gt: min_id,
|
gt: min_id,
|
||||||
gte: since_id,
|
gte: since_id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
take: Number(limit),
|
take: Number(limit),
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: "desc",
|
id: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Constuct HTTP Link header (next and prev)
|
// Constuct HTTP Link header (next and prev)
|
||||||
const linkHeader = [];
|
const linkHeader = [];
|
||||||
if (objects.length > 0) {
|
if (objects.length > 0) {
|
||||||
const urlWithoutQuery = req.url.split("?")[0];
|
const urlWithoutQuery = req.url.split("?")[0];
|
||||||
linkHeader.push(
|
linkHeader.push(
|
||||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
await Promise.all(objects.map(status => statusToAPI(status, user))),
|
await Promise.all(
|
||||||
200,
|
objects.map((status) => statusToAPI(status, user)),
|
||||||
{
|
),
|
||||||
Link: linkHeader.join(", "),
|
200,
|
||||||
}
|
{
|
||||||
);
|
Link: linkHeader.join(", "),
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const objects = await client.status.findMany({
|
const objects = await client.status.findMany({
|
||||||
where: {
|
where: {
|
||||||
authorId: id,
|
authorId: id,
|
||||||
isReblog: exclude_reblogs ? true : undefined,
|
isReblog: exclude_reblogs ? true : undefined,
|
||||||
id: {
|
id: {
|
||||||
lt: max_id,
|
lt: max_id,
|
||||||
gt: min_id,
|
gt: min_id,
|
||||||
gte: since_id,
|
gte: since_id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
take: Number(limit),
|
take: Number(limit),
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: "desc",
|
id: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Constuct HTTP Link header (next and prev)
|
// Constuct HTTP Link header (next and prev)
|
||||||
const linkHeader = [];
|
const linkHeader = [];
|
||||||
if (objects.length > 0) {
|
if (objects.length > 0) {
|
||||||
const urlWithoutQuery = req.url.split("?")[0];
|
const urlWithoutQuery = req.url.split("?")[0];
|
||||||
linkHeader.push(
|
linkHeader.push(
|
||||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
await Promise.all(objects.map(status => statusToAPI(status, user))),
|
await Promise.all(objects.map((status) => statusToAPI(status, user))),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: linkHeader.join(", "),
|
Link: linkHeader.join(", "),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,81 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/unblock",
|
route: "/accounts/:id/unblock",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:blocks"],
|
oauthPermissions: ["write:blocks"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocks a user
|
* Blocks a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relationship.blocking) {
|
if (relationship.blocking) {
|
||||||
relationship.blocking = false;
|
relationship.blocking = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
blocking: false,
|
blocking: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,81 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/unfollow",
|
route: "/accounts/:id/unfollow",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:follows"],
|
oauthPermissions: ["write:follows"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unfollows a user
|
* Unfollows a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relationship.following) {
|
if (relationship.following) {
|
||||||
relationship.following = false;
|
relationship.following = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
following: false,
|
following: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,83 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/unmute",
|
route: "/accounts/:id/unmute",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:mutes"],
|
oauthPermissions: ["write:mutes"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unmute a user
|
* Unmute a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relationship.muting) {
|
if (relationship.muting) {
|
||||||
relationship.muting = false;
|
relationship.muting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement duration
|
// TODO: Implement duration
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
muting: false,
|
muting: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,81 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { client } from "~database/datasource";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/accounts/:id/unpin",
|
route: "/accounts/:id/unpin",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:accounts"],
|
oauthPermissions: ["write:accounts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unpin a user
|
* Unpin a user
|
||||||
*/
|
*/
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const user = await client.user.findUnique({
|
const user = await client.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
relationships: {
|
relationships: {
|
||||||
include: {
|
include: {
|
||||||
owner: true,
|
owner: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
let relationship = await getRelationshipToOtherUser(self, user);
|
let relationship = await getRelationshipToOtherUser(self, user);
|
||||||
|
|
||||||
if (!relationship) {
|
if (!relationship) {
|
||||||
// Create new relationship
|
// Create new relationship
|
||||||
|
|
||||||
const newRelationship = await createNewRelationship(self, user);
|
const newRelationship = await createNewRelationship(self, user);
|
||||||
|
|
||||||
await client.user.update({
|
await client.user.update({
|
||||||
where: { id: self.id },
|
where: { id: self.id },
|
||||||
data: {
|
data: {
|
||||||
relationships: {
|
relationships: {
|
||||||
connect: {
|
connect: {
|
||||||
id: newRelationship.id,
|
id: newRelationship.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship = newRelationship;
|
relationship = newRelationship;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relationship.endorsed) {
|
if (relationship.endorsed) {
|
||||||
relationship.endorsed = false;
|
relationship.endorsed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.relationship.update({
|
await client.relationship.update({
|
||||||
where: { id: relationship.id },
|
where: { id: relationship.id },
|
||||||
data: {
|
data: {
|
||||||
endorsed: false,
|
endorsed: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,67 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import { userToAPI } from "~database/entities/User";
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/accounts/familiar_followers",
|
route: "/api/v1/accounts/familiar_followers",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 5,
|
max: 5,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["read:follows"],
|
oauthPermissions: ["read:follows"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find familiar followers (followers of a user that you also follow)
|
* Find familiar followers (followers of a user that you also follow)
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
id: string[];
|
id: string[];
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const { id: ids } = extraData.parsedRequest;
|
const { id: ids } = extraData.parsedRequest;
|
||||||
|
|
||||||
// Minimum id count 1, maximum 10
|
// Minimum id count 1, maximum 10
|
||||||
if (!ids || ids.length < 1 || ids.length > 10) {
|
if (!ids || ids.length < 1 || ids.length > 10) {
|
||||||
return errorResponse("Number of ids must be between 1 and 10", 422);
|
return errorResponse("Number of ids must be between 1 and 10", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
const followersOfIds = await client.user.findMany({
|
const followersOfIds = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
relationships: {
|
relationships: {
|
||||||
some: {
|
some: {
|
||||||
subjectId: {
|
subjectId: {
|
||||||
in: ids,
|
in: ids,
|
||||||
},
|
},
|
||||||
following: true,
|
following: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find users that you follow in followersOfIds
|
// Find users that you follow in followersOfIds
|
||||||
const output = await client.user.findMany({
|
const output = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
relationships: {
|
relationships: {
|
||||||
some: {
|
some: {
|
||||||
ownerId: self.id,
|
ownerId: self.id,
|
||||||
subjectId: {
|
subjectId: {
|
||||||
in: followersOfIds.map(f => f.id),
|
in: followersOfIds.map((f) => f.id),
|
||||||
},
|
},
|
||||||
following: true,
|
following: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(output.map(o => userToAPI(o)));
|
return jsonResponse(output.map((o) => userToAPI(o)));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,202 +1,206 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { jsonResponse } from "@response";
|
import { jsonResponse } from "@response";
|
||||||
import { tempmailDomains } from "@tempmail";
|
import { tempmailDomains } from "@tempmail";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import ISO6391 from "iso-639-1";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
import { createNewLocalUser } from "~database/entities/User";
|
import { createNewLocalUser } from "~database/entities/User";
|
||||||
import ISO6391 from "iso-639-1";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
route: "/api/v1/accounts",
|
route: "/api/v1/accounts",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 2,
|
max: 2,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
oauthPermissions: ["write:accounts"],
|
oauthPermissions: ["write:accounts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
agreement: boolean;
|
agreement: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
// TODO: Add Authorization check
|
// TODO: Add Authorization check
|
||||||
|
|
||||||
const body = extraData.parsedRequest;
|
const body = extraData.parsedRequest;
|
||||||
|
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
|
|
||||||
if (!config.signups.registration) {
|
if (!config.signups.registration) {
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
{
|
{
|
||||||
error: "Registration is disabled",
|
error: "Registration is disabled",
|
||||||
},
|
},
|
||||||
422
|
422,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors: {
|
const errors: {
|
||||||
details: Record<
|
details: Record<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
error:
|
error:
|
||||||
| "ERR_BLANK"
|
| "ERR_BLANK"
|
||||||
| "ERR_INVALID"
|
| "ERR_INVALID"
|
||||||
| "ERR_TOO_LONG"
|
| "ERR_TOO_LONG"
|
||||||
| "ERR_TOO_SHORT"
|
| "ERR_TOO_SHORT"
|
||||||
| "ERR_BLOCKED"
|
| "ERR_BLOCKED"
|
||||||
| "ERR_TAKEN"
|
| "ERR_TAKEN"
|
||||||
| "ERR_RESERVED"
|
| "ERR_RESERVED"
|
||||||
| "ERR_ACCEPTED"
|
| "ERR_ACCEPTED"
|
||||||
| "ERR_INCLUSION";
|
| "ERR_INCLUSION";
|
||||||
description: string;
|
description: string;
|
||||||
}[]
|
}[]
|
||||||
>;
|
>;
|
||||||
} = {
|
} = {
|
||||||
details: {
|
details: {
|
||||||
password: [],
|
password: [],
|
||||||
username: [],
|
username: [],
|
||||||
email: [],
|
email: [],
|
||||||
agreement: [],
|
agreement: [],
|
||||||
locale: [],
|
locale: [],
|
||||||
reason: [],
|
reason: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if fields are blank
|
// Check if fields are blank
|
||||||
["username", "email", "password", "agreement", "locale", "reason"].forEach(
|
for (const value of [
|
||||||
value => {
|
"username",
|
||||||
// @ts-expect-error Value is always valid
|
"email",
|
||||||
if (!body[value])
|
"password",
|
||||||
errors.details[value].push({
|
"agreement",
|
||||||
error: "ERR_BLANK",
|
"locale",
|
||||||
description: `can't be blank`,
|
"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
|
// Check if username is valid
|
||||||
if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
|
if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_INVALID",
|
error: "ERR_INVALID",
|
||||||
description: `must only contain letters, numbers, and underscores`,
|
description: "must only contain letters, numbers, and underscores",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if username doesnt match filters
|
// Check if username doesnt match filters
|
||||||
if (
|
if (
|
||||||
config.filters.username_filters.some(filter =>
|
config.filters.username.some((filter) => body.username?.match(filter))
|
||||||
body.username?.match(filter)
|
) {
|
||||||
)
|
errors.details.username.push({
|
||||||
) {
|
error: "ERR_INVALID",
|
||||||
errors.details.username.push({
|
description: "contains blocked words",
|
||||||
error: "ERR_INVALID",
|
});
|
||||||
description: `contains blocked words`,
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if username is too long
|
// Check if username is too long
|
||||||
if ((body.username?.length ?? 0) > config.validation.max_username_size)
|
if ((body.username?.length ?? 0) > config.validation.max_username_size)
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_TOO_LONG",
|
error: "ERR_TOO_LONG",
|
||||||
description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
|
description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if username is too short
|
// Check if username is too short
|
||||||
if ((body.username?.length ?? 0) < 3)
|
if ((body.username?.length ?? 0) < 3)
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_TOO_SHORT",
|
error: "ERR_TOO_SHORT",
|
||||||
description: `is too short (minimum is 3 characters)`,
|
description: "is too short (minimum is 3 characters)",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if username is reserved
|
// Check if username is reserved
|
||||||
if (config.validation.username_blacklist.includes(body.username ?? ""))
|
if (config.validation.username_blacklist.includes(body.username ?? ""))
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_RESERVED",
|
error: "ERR_RESERVED",
|
||||||
description: `is reserved`,
|
description: "is reserved",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if username is taken
|
// Check if username is taken
|
||||||
if (await client.user.findFirst({ where: { username: body.username } }))
|
if (await client.user.findFirst({ where: { username: body.username } }))
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_TAKEN",
|
error: "ERR_TAKEN",
|
||||||
description: `is already taken`,
|
description: "is already taken",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if email is valid
|
// Check if email is valid
|
||||||
if (
|
if (
|
||||||
!body.email?.match(
|
!body.email?.match(
|
||||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
errors.details.email.push({
|
errors.details.email.push({
|
||||||
error: "ERR_INVALID",
|
error: "ERR_INVALID",
|
||||||
description: `must be a valid email address`,
|
description: "must be a valid email address",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if email is blocked
|
// Check if email is blocked
|
||||||
if (
|
if (
|
||||||
config.validation.email_blacklist.includes(body.email ?? "") ||
|
config.validation.email_blacklist.includes(body.email ?? "") ||
|
||||||
(config.validation.blacklist_tempmail &&
|
(config.validation.blacklist_tempmail &&
|
||||||
tempmailDomains.domains.includes((body.email ?? "").split("@")[1]))
|
tempmailDomains.domains.includes((body.email ?? "").split("@")[1]))
|
||||||
)
|
)
|
||||||
errors.details.email.push({
|
errors.details.email.push({
|
||||||
error: "ERR_BLOCKED",
|
error: "ERR_BLOCKED",
|
||||||
description: `is from a blocked email provider`,
|
description: "is from a blocked email provider",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if agreement is accepted
|
// Check if agreement is accepted
|
||||||
if (!body.agreement)
|
if (!body.agreement)
|
||||||
errors.details.agreement.push({
|
errors.details.agreement.push({
|
||||||
error: "ERR_ACCEPTED",
|
error: "ERR_ACCEPTED",
|
||||||
description: `must be accepted`,
|
description: "must be accepted",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!body.locale)
|
if (!body.locale)
|
||||||
errors.details.locale.push({
|
errors.details.locale.push({
|
||||||
error: "ERR_BLANK",
|
error: "ERR_BLANK",
|
||||||
description: `can't be blank`,
|
description: `can't be blank`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ISO6391.validate(body.locale ?? ""))
|
if (!ISO6391.validate(body.locale ?? ""))
|
||||||
errors.details.locale.push({
|
errors.details.locale.push({
|
||||||
error: "ERR_INVALID",
|
error: "ERR_INVALID",
|
||||||
description: `must be a valid ISO 639-1 code`,
|
description: "must be a valid ISO 639-1 code",
|
||||||
});
|
});
|
||||||
|
|
||||||
// If any errors are present, return them
|
// If any errors are present, return them
|
||||||
if (Object.values(errors.details).some(value => value.length > 0)) {
|
if (Object.values(errors.details).some((value) => value.length > 0)) {
|
||||||
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
|
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
|
||||||
|
|
||||||
const errorsText = Object.entries(errors.details)
|
const errorsText = Object.entries(errors.details)
|
||||||
.map(
|
.map(
|
||||||
([name, errors]) =>
|
([name, errors]) =>
|
||||||
`${name} ${errors
|
`${name} ${errors
|
||||||
.map(error => error.description)
|
.map((error) => error.description)
|
||||||
.join(", ")}`
|
.join(", ")}`,
|
||||||
)
|
)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
{
|
{
|
||||||
error: `Validation failed: ${errorsText}`,
|
error: `Validation failed: ${errorsText}`,
|
||||||
details: errors.details,
|
details: errors.details,
|
||||||
},
|
},
|
||||||
422
|
422,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await createNewLocalUser({
|
await createNewLocalUser({
|
||||||
username: body.username ?? "",
|
username: body.username ?? "",
|
||||||
password: body.password ?? "",
|
password: body.password ?? "",
|
||||||
email: body.email ?? "",
|
email: body.email ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Response("", {
|
return new Response("", {
|
||||||
status: 200,
|
status: 200,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,67 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import {
|
|
||||||
createNewRelationship,
|
|
||||||
relationshipToAPI,
|
|
||||||
} from "~database/entities/Relationship";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import type { User } from "@prisma/client";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import {
|
||||||
|
createNewRelationship,
|
||||||
|
relationshipToAPI,
|
||||||
|
} from "~database/entities/Relationship";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/accounts/relationships",
|
route: "/api/v1/accounts/relationships",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 30,
|
max: 30,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["read:follows"],
|
oauthPermissions: ["read:follows"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find relationships
|
* Find relationships
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
id: string[];
|
id: string[];
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const { user: self } = extraData.auth;
|
const { user: self } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const { id: ids } = extraData.parsedRequest;
|
const { id: ids } = extraData.parsedRequest;
|
||||||
|
|
||||||
// Minimum id count 1, maximum 10
|
// Minimum id count 1, maximum 10
|
||||||
if (!ids || ids.length < 1 || ids.length > 10) {
|
if (!ids || ids.length < 1 || ids.length > 10) {
|
||||||
return errorResponse("Number of ids must be between 1 and 10", 422);
|
return errorResponse("Number of ids must be between 1 and 10", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relationships = await client.relationship.findMany({
|
const relationships = await client.relationship.findMany({
|
||||||
where: {
|
where: {
|
||||||
ownerId: self.id,
|
ownerId: self.id,
|
||||||
subjectId: {
|
subjectId: {
|
||||||
in: ids,
|
in: ids,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find IDs that dont have a relationship
|
// Find IDs that dont have a relationship
|
||||||
const missingIds = ids.filter(
|
const missingIds = ids.filter(
|
||||||
id => !relationships.some(r => r.subjectId === id)
|
(id) => !relationships.some((r) => r.subjectId === id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the missing relationships
|
// Create the missing relationships
|
||||||
for (const id of missingIds) {
|
for (const id of missingIds) {
|
||||||
const relationship = await createNewRelationship(self, { id } as any);
|
const relationship = await createNewRelationship(self, { id } as User);
|
||||||
|
|
||||||
relationships.push(relationship);
|
relationships.push(relationship);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order in the same order as ids
|
// Order in the same order as ids
|
||||||
relationships.sort(
|
relationships.sort(
|
||||||
(a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId)
|
(a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId),
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonResponse(relationships.map(r => relationshipToAPI(r)));
|
return jsonResponse(relationships.map((r) => relationshipToAPI(r)));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,75 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import { userToAPI } from "~database/entities/User";
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/accounts/search",
|
route: "/api/v1/accounts/search",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["read:accounts"],
|
oauthPermissions: ["read:accounts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
q?: string;
|
q?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
resolve?: boolean;
|
resolve?: boolean;
|
||||||
following?: boolean;
|
following?: boolean;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
// TODO: Add checks for disabled or not email verified accounts
|
// TODO: Add checks for disabled or not email verified accounts
|
||||||
|
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
following = false,
|
following = false,
|
||||||
limit = 40,
|
limit = 40,
|
||||||
offset,
|
offset,
|
||||||
q,
|
q,
|
||||||
} = extraData.parsedRequest;
|
} = extraData.parsedRequest;
|
||||||
|
|
||||||
if (limit < 1 || limit > 80) {
|
if (limit < 1 || limit > 80) {
|
||||||
return errorResponse("Limit must be between 1 and 80", 400);
|
return errorResponse("Limit must be between 1 and 80", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add WebFinger resolve
|
// TODO: Add WebFinger resolve
|
||||||
|
|
||||||
const accounts = await client.user.findMany({
|
const accounts = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
displayName: {
|
displayName: {
|
||||||
contains: q,
|
contains: q,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
username: {
|
username: {
|
||||||
contains: q,
|
contains: q,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
relationshipSubjects: following
|
relationshipSubjects: following
|
||||||
? {
|
? {
|
||||||
some: {
|
some: {
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
following,
|
following,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
take: Number(limit),
|
take: Number(limit),
|
||||||
skip: Number(offset || 0),
|
skip: Number(offset || 0),
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(accounts.map(acct => userToAPI(acct)));
|
return jsonResponse(accounts.map((acct) => userToAPI(acct)));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,72 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { sanitize } from "isomorphic-dompurify";
|
import { convertTextToHtml } from "@formatting";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { sanitizeHtml } from "@sanitization";
|
import { sanitizeHtml } from "@sanitization";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { parseEmojis } from "~database/entities/Emoji";
|
import { sanitize } from "isomorphic-dompurify";
|
||||||
import { client } from "~database/datasource";
|
|
||||||
import type { APISource } from "~types/entities/source";
|
|
||||||
import { convertTextToHtml } from "@formatting";
|
|
||||||
import { MediaBackendType } from "media-manager";
|
import { MediaBackendType } from "media-manager";
|
||||||
import type { MediaBackend } from "media-manager";
|
import type { MediaBackend } from "media-manager";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
import { getUrl } from "~database/entities/Attachment";
|
||||||
|
import { parseEmojis } from "~database/entities/Emoji";
|
||||||
|
import { userToAPI } from "~database/entities/User";
|
||||||
|
import { userRelations } from "~database/entities/relations";
|
||||||
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
||||||
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||||
import { getUrl } from "~database/entities/Attachment";
|
import type { APISource } from "~types/entities/source";
|
||||||
import { userRelations } from "~database/entities/relations";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["PATCH"],
|
allowedMethods: ["PATCH"],
|
||||||
route: "/api/v1/accounts/update_credentials",
|
route: "/api/v1/accounts/update_credentials",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 2,
|
max: 2,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:accounts"],
|
oauthPermissions: ["write:accounts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
display_name: string;
|
display_name: string;
|
||||||
note: string;
|
note: string;
|
||||||
avatar: File;
|
avatar: File;
|
||||||
header: File;
|
header: File;
|
||||||
locked: string;
|
locked: string;
|
||||||
bot: string;
|
bot: string;
|
||||||
discoverable: string;
|
discoverable: string;
|
||||||
"source[privacy]": string;
|
"source[privacy]": string;
|
||||||
"source[sensitive]": string;
|
"source[sensitive]": string;
|
||||||
"source[language]": string;
|
"source[language]": string;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
display_name,
|
display_name,
|
||||||
note,
|
note,
|
||||||
avatar,
|
avatar,
|
||||||
header,
|
header,
|
||||||
locked,
|
locked,
|
||||||
bot,
|
bot,
|
||||||
discoverable,
|
discoverable,
|
||||||
"source[privacy]": source_privacy,
|
"source[privacy]": source_privacy,
|
||||||
"source[sensitive]": source_sensitive,
|
"source[sensitive]": source_sensitive,
|
||||||
"source[language]": source_language,
|
"source[language]": source_language,
|
||||||
} = extraData.parsedRequest;
|
} = extraData.parsedRequest;
|
||||||
|
|
||||||
const sanitizedNote = await sanitizeHtml(note ?? "");
|
const sanitizedNote = await sanitizeHtml(note ?? "");
|
||||||
|
|
||||||
const sanitizedDisplayName = sanitize(display_name ?? "", {
|
const sanitizedDisplayName = sanitize(display_name ?? "", {
|
||||||
ALLOWED_TAGS: [],
|
ALLOWED_TAGS: [],
|
||||||
ALLOWED_ATTR: [],
|
ALLOWED_ATTR: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
/* if (!user.source) {
|
/* if (!user.source) {
|
||||||
user.source = {
|
user.source = {
|
||||||
privacy: "public",
|
privacy: "public",
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
|
|
@ -75,191 +75,192 @@ export default apiRoute<{
|
||||||
};
|
};
|
||||||
} */
|
} */
|
||||||
|
|
||||||
let mediaManager: MediaBackend;
|
let mediaManager: MediaBackend;
|
||||||
|
|
||||||
switch (config.media.backend as MediaBackendType) {
|
switch (config.media.backend as MediaBackendType) {
|
||||||
case MediaBackendType.LOCAL:
|
case MediaBackendType.LOCAL:
|
||||||
mediaManager = new LocalMediaBackend(config);
|
mediaManager = new LocalMediaBackend(config);
|
||||||
break;
|
break;
|
||||||
case MediaBackendType.S3:
|
case MediaBackendType.S3:
|
||||||
mediaManager = new S3MediaBackend(config);
|
mediaManager = new S3MediaBackend(config);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// TODO: Replace with logger
|
// TODO: Replace with logger
|
||||||
throw new Error("Invalid media backend");
|
throw new Error("Invalid media backend");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display_name) {
|
if (display_name) {
|
||||||
// Check if within allowed display name lengths
|
// Check if within allowed display name lengths
|
||||||
if (
|
if (
|
||||||
sanitizedDisplayName.length < 3 ||
|
sanitizedDisplayName.length < 3 ||
|
||||||
sanitizedDisplayName.length > config.validation.max_displayname_size
|
sanitizedDisplayName.length > config.validation.max_displayname_size
|
||||||
) {
|
) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`Display name must be between 3 and ${config.validation.max_displayname_size} characters`,
|
`Display name must be between 3 and ${config.validation.max_displayname_size} characters`,
|
||||||
422
|
422,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if display name doesnt match filters
|
// Check if display name doesnt match filters
|
||||||
if (
|
if (
|
||||||
config.filters.displayname.some(filter =>
|
config.filters.displayname.some((filter) =>
|
||||||
sanitizedDisplayName.match(filter)
|
sanitizedDisplayName.match(filter),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return errorResponse("Display name contains blocked words", 422);
|
return errorResponse("Display name contains blocked words", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove emojis
|
// Remove emojis
|
||||||
user.emojis = [];
|
user.emojis = [];
|
||||||
|
|
||||||
user.displayName = sanitizedDisplayName;
|
user.displayName = sanitizedDisplayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note && user.source) {
|
if (note && user.source) {
|
||||||
// Check if within allowed note length
|
// Check if within allowed note length
|
||||||
if (sanitizedNote.length > config.validation.max_note_size) {
|
if (sanitizedNote.length > config.validation.max_note_size) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`Note must be less than ${config.validation.max_note_size} characters`,
|
`Note must be less than ${config.validation.max_note_size} characters`,
|
||||||
422
|
422,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bio doesnt match filters
|
// Check if bio doesnt match filters
|
||||||
if (config.filters.bio.some(filter => sanitizedNote.match(filter))) {
|
if (config.filters.bio.some((filter) => sanitizedNote.match(filter))) {
|
||||||
return errorResponse("Bio contains blocked words", 422);
|
return errorResponse("Bio contains blocked words", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
(user.source as APISource).note = sanitizedNote;
|
(user.source as APISource).note = sanitizedNote;
|
||||||
// TODO: Convert note to HTML
|
// TODO: Convert note to HTML
|
||||||
user.note = await convertTextToHtml(sanitizedNote);
|
user.note = await convertTextToHtml(sanitizedNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source_privacy && user.source) {
|
if (source_privacy && user.source) {
|
||||||
// Check if within allowed privacy values
|
// Check if within allowed privacy values
|
||||||
if (
|
if (
|
||||||
!["public", "unlisted", "private", "direct"].includes(
|
!["public", "unlisted", "private", "direct"].includes(
|
||||||
source_privacy
|
source_privacy,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
"Privacy must be one of public, unlisted, private, or direct",
|
"Privacy must be one of public, unlisted, private, or direct",
|
||||||
422
|
422,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
(user.source as APISource).privacy = source_privacy;
|
(user.source as APISource).privacy = source_privacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source_sensitive && user.source) {
|
if (source_sensitive && user.source) {
|
||||||
// Check if within allowed sensitive values
|
// Check if within allowed sensitive values
|
||||||
if (source_sensitive !== "true" && source_sensitive !== "false") {
|
if (source_sensitive !== "true" && source_sensitive !== "false") {
|
||||||
return errorResponse("Sensitive must be a boolean", 422);
|
return errorResponse("Sensitive must be a boolean", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
(user.source as APISource).sensitive = source_sensitive === "true";
|
(user.source as APISource).sensitive = source_sensitive === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source_language && user.source) {
|
if (source_language && user.source) {
|
||||||
if (!ISO6391.validate(source_language)) {
|
if (!ISO6391.validate(source_language)) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
"Language must be a valid ISO 639-1 code",
|
"Language must be a valid ISO 639-1 code",
|
||||||
422
|
422,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
(user.source as APISource).language = source_language;
|
(user.source as APISource).language = source_language;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
// Check if within allowed avatar length (avatar is an image)
|
// Check if within allowed avatar length (avatar is an image)
|
||||||
if (avatar.size > config.validation.max_avatar_size) {
|
if (avatar.size > config.validation.max_avatar_size) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
|
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
|
||||||
422
|
422,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uploadedFile } = await mediaManager.addFile(avatar);
|
const { uploadedFile } = await mediaManager.addFile(avatar);
|
||||||
|
|
||||||
user.avatar = getUrl(uploadedFile.name, config);
|
user.avatar = getUrl(uploadedFile.name, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
// Check if within allowed header length (header is an image)
|
// Check if within allowed header length (header is an image)
|
||||||
if (header.size > config.validation.max_header_size) {
|
if (header.size > config.validation.max_header_size) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`Header must be less than ${config.validation.max_avatar_size} bytes`,
|
`Header must be less than ${config.validation.max_avatar_size} bytes`,
|
||||||
422
|
422,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uploadedFile } = await mediaManager.addFile(header);
|
const { uploadedFile } = await mediaManager.addFile(header);
|
||||||
|
|
||||||
user.header = getUrl(uploadedFile.name, config);
|
user.header = getUrl(uploadedFile.name, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locked) {
|
if (locked) {
|
||||||
// Check if locked is a boolean
|
// Check if locked is a boolean
|
||||||
if (locked !== "true" && locked !== "false") {
|
if (locked !== "true" && locked !== "false") {
|
||||||
return errorResponse("Locked must be a boolean", 422);
|
return errorResponse("Locked must be a boolean", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.isLocked = locked === "true";
|
user.isLocked = locked === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bot) {
|
if (bot) {
|
||||||
// Check if bot is a boolean
|
// Check if bot is a boolean
|
||||||
if (bot !== "true" && bot !== "false") {
|
if (bot !== "true" && bot !== "false") {
|
||||||
return errorResponse("Bot must be a boolean", 422);
|
return errorResponse("Bot must be a boolean", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.isBot = bot === "true";
|
user.isBot = bot === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (discoverable) {
|
if (discoverable) {
|
||||||
// Check if discoverable is a boolean
|
// Check if discoverable is a boolean
|
||||||
if (discoverable !== "true" && discoverable !== "false") {
|
if (discoverable !== "true" && discoverable !== "false") {
|
||||||
return errorResponse("Discoverable must be a boolean", 422);
|
return errorResponse("Discoverable must be a boolean", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.isDiscoverable = discoverable === "true";
|
user.isDiscoverable = discoverable === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse emojis
|
// Parse emojis
|
||||||
|
|
||||||
const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
|
const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
|
||||||
const noteEmojis = await parseEmojis(sanitizedNote);
|
const noteEmojis = await parseEmojis(sanitizedNote);
|
||||||
|
|
||||||
user.emojis = [...displaynameEmojis, ...noteEmojis];
|
user.emojis = [...displaynameEmojis, ...noteEmojis];
|
||||||
|
|
||||||
// Deduplicate emojis
|
// Deduplicate emojis
|
||||||
user.emojis = user.emojis.filter(
|
user.emojis = user.emojis.filter(
|
||||||
(emoji, index, self) => self.findIndex(e => e.id === emoji.id) === index
|
(emoji, index, self) =>
|
||||||
);
|
self.findIndex((e) => e.id === emoji.id) === index,
|
||||||
|
);
|
||||||
|
|
||||||
const output = await client.user.update({
|
const output = await client.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: {
|
data: {
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
note: user.note,
|
note: user.note,
|
||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
header: user.header,
|
header: user.header,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isBot: user.isBot,
|
isBot: user.isBot,
|
||||||
isDiscoverable: user.isDiscoverable,
|
isDiscoverable: user.isDiscoverable,
|
||||||
emojis: {
|
emojis: {
|
||||||
disconnect: user.emojis.map(e => ({
|
disconnect: user.emojis.map((e) => ({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
})),
|
})),
|
||||||
connect: user.emojis.map(e => ({
|
connect: user.emojis.map((e) => ({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
source: user.source || undefined,
|
source: user.source || undefined,
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(userToAPI(output));
|
return jsonResponse(userToAPI(output));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { userToAPI } from "~database/entities/User";
|
import { userToAPI } from "~database/entities/User";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/accounts/verify_credentials",
|
route: "/api/v1/accounts/verify_credentials",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["read:accounts"],
|
oauthPermissions: ["read:accounts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute((req, matchedRoute, extraData) => {
|
export default apiRoute((req, matchedRoute, extraData) => {
|
||||||
// TODO: Add checks for disabled or not email verified accounts
|
// TODO: Add checks for disabled or not email verified accounts
|
||||||
|
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
...userToAPI(user, true),
|
...userToAPI(user, true),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,65 @@
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { randomBytes } from "crypto";
|
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
route: "/api/v1/apps",
|
route: "/api/v1/apps",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 2,
|
max: 2,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new application to obtain OAuth 2 credentials
|
* Creates a new application to obtain OAuth 2 credentials
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
client_name: string;
|
client_name: string;
|
||||||
redirect_uris: string;
|
redirect_uris: string;
|
||||||
scopes: string;
|
scopes: string;
|
||||||
website: string;
|
website: string;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const { client_name, redirect_uris, scopes, website } =
|
const { client_name, redirect_uris, scopes, website } =
|
||||||
extraData.parsedRequest;
|
extraData.parsedRequest;
|
||||||
|
|
||||||
// Check if redirect URI is a valid URI, and also an absolute URI
|
// Check if redirect URI is a valid URI, and also an absolute URI
|
||||||
if (redirect_uris) {
|
if (redirect_uris) {
|
||||||
try {
|
try {
|
||||||
const redirect_uri = new URL(redirect_uris);
|
const redirect_uri = new URL(redirect_uris);
|
||||||
|
|
||||||
if (!redirect_uri.protocol.startsWith("http")) {
|
if (!redirect_uri.protocol.startsWith("http")) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
"Redirect URI must be an absolute URI",
|
"Redirect URI must be an absolute URI",
|
||||||
422
|
422,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return errorResponse("Redirect URI must be a valid URI", 422);
|
return errorResponse("Redirect URI must be a valid URI", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const application = await client.application.create({
|
const application = await client.application.create({
|
||||||
data: {
|
data: {
|
||||||
name: client_name || "",
|
name: client_name || "",
|
||||||
redirect_uris: redirect_uris || "",
|
redirect_uris: redirect_uris || "",
|
||||||
scopes: scopes || "read",
|
scopes: scopes || "read",
|
||||||
website: website || null,
|
website: website || null,
|
||||||
client_id: randomBytes(32).toString("base64url"),
|
client_id: randomBytes(32).toString("base64url"),
|
||||||
secret: randomBytes(64).toString("base64url"),
|
secret: randomBytes(64).toString("base64url"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
id: application.id,
|
id: application.id,
|
||||||
name: application.name,
|
name: application.name,
|
||||||
website: application.website,
|
website: application.website,
|
||||||
client_id: application.client_id,
|
client_id: application.client_id,
|
||||||
client_secret: application.secret,
|
client_secret: application.secret,
|
||||||
redirect_uri: application.redirect_uris,
|
redirect_uri: application.redirect_uris,
|
||||||
vapid_link: application.vapid_key,
|
vapid_link: application.vapid_key,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,32 @@ import { errorResponse, jsonResponse } from "@response";
|
||||||
import { getFromToken } from "~database/entities/Application";
|
import { getFromToken } from "~database/entities/Application";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/apps/verify_credentials",
|
route: "/api/v1/apps/verify_credentials",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns OAuth2 credentials
|
* Returns OAuth2 credentials
|
||||||
*/
|
*/
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const { user, token } = extraData.auth;
|
const { user, token } = extraData.auth;
|
||||||
const application = await getFromToken(token);
|
const application = await getFromToken(token);
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
if (!application) return errorResponse("Unauthorized", 401);
|
if (!application) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
name: application.name,
|
name: application.name,
|
||||||
website: application.website,
|
website: application.website,
|
||||||
vapid_key: application.vapid_key,
|
vapid_key: application.vapid_key,
|
||||||
redirect_uris: application.redirect_uris,
|
redirect_uris: application.redirect_uris,
|
||||||
scopes: application.scopes,
|
scopes: application.scopes,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import { userToAPI } from "~database/entities/User";
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/blocks",
|
route: "/api/v1/blocks",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const blocks = await client.user.findMany({
|
const blocks = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
relationshipSubjects: {
|
relationshipSubjects: {
|
||||||
some: {
|
some: {
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(blocks.map(u => userToAPI(u)));
|
return jsonResponse(blocks.map((u) => userToAPI(u)));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,25 @@ import { client } from "~database/datasource";
|
||||||
import { emojiToAPI } from "~database/entities/Emoji";
|
import { emojiToAPI } from "~database/entities/Emoji";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/custom_emojis",
|
route: "/api/v1/custom_emojis",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute(async () => {
|
export default apiRoute(async () => {
|
||||||
const emojis = await client.emoji.findMany({
|
const emojis = await client.emoji.findMany({
|
||||||
where: {
|
where: {
|
||||||
instanceId: null,
|
instanceId: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
await Promise.all(emojis.map(emoji => emojiToAPI(emoji)))
|
await Promise.all(emojis.map((emoji) => emojiToAPI(emoji))),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,74 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
import { statusAndUserRelations } from "~database/entities/relations";
|
|
||||||
import { statusToAPI } from "~database/entities/Status";
|
import { statusToAPI } from "~database/entities/Status";
|
||||||
|
import { statusAndUserRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/favourites",
|
route: "/api/v1/favourites",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
max_id?: string;
|
max_id?: string;
|
||||||
since_id?: string;
|
since_id?: string;
|
||||||
min_id?: string;
|
min_id?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
||||||
|
|
||||||
if (limit < 1 || limit > 40) {
|
if (limit < 1 || limit > 40) {
|
||||||
return errorResponse("Limit must be between 1 and 40", 400);
|
return errorResponse("Limit must be between 1 and 40", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const objects = await client.status.findMany({
|
const objects = await client.status.findMany({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
lt: max_id ?? undefined,
|
lt: max_id ?? undefined,
|
||||||
gte: since_id ?? undefined,
|
gte: since_id ?? undefined,
|
||||||
gt: min_id ?? undefined,
|
gt: min_id ?? undefined,
|
||||||
},
|
},
|
||||||
likes: {
|
likes: {
|
||||||
some: {
|
some: {
|
||||||
likerId: user.id,
|
likerId: user.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: "desc",
|
id: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Constuct HTTP Link header (next and prev)
|
// Constuct HTTP Link header (next and prev)
|
||||||
const linkHeader = [];
|
const linkHeader = [];
|
||||||
if (objects.length > 0) {
|
if (objects.length > 0) {
|
||||||
const urlWithoutQuery = req.url.split("?")[0];
|
const urlWithoutQuery = req.url.split("?")[0];
|
||||||
linkHeader.push(
|
linkHeader.push(
|
||||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
objects.map(async status => statusToAPI(status, user))
|
objects.map(async (status) => statusToAPI(status, user)),
|
||||||
),
|
),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: linkHeader.join(", "),
|
Link: linkHeader.join(", "),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,75 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
checkForBidirectionalRelationships,
|
checkForBidirectionalRelationships,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
route: "/api/v1/follow_requests/:account_id/authorize",
|
route: "/api/v1/follow_requests/:account_id/authorize",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const { account_id } = matchedRoute.params;
|
const { account_id } = matchedRoute.params;
|
||||||
|
|
||||||
const account = await client.user.findUnique({
|
const account = await client.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: account_id,
|
id: account_id,
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!account) return errorResponse("Account not found", 404);
|
if (!account) return errorResponse("Account not found", 404);
|
||||||
|
|
||||||
// Check if there is a relationship on both sides
|
// Check if there is a relationship on both sides
|
||||||
await checkForBidirectionalRelationships(user, account);
|
await checkForBidirectionalRelationships(user, account);
|
||||||
|
|
||||||
// Authorize follow request
|
// Authorize follow request
|
||||||
await client.relationship.updateMany({
|
await client.relationship.updateMany({
|
||||||
where: {
|
where: {
|
||||||
subjectId: user.id,
|
subjectId: user.id,
|
||||||
ownerId: account.id,
|
ownerId: account.id,
|
||||||
requested: true,
|
requested: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
requested: false,
|
requested: false,
|
||||||
following: true,
|
following: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update followedBy for other user
|
// Update followedBy for other user
|
||||||
await client.relationship.updateMany({
|
await client.relationship.updateMany({
|
||||||
where: {
|
where: {
|
||||||
subjectId: account.id,
|
subjectId: account.id,
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
followedBy: true,
|
followedBy: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const relationship = await client.relationship.findFirst({
|
const relationship = await client.relationship.findFirst({
|
||||||
where: {
|
where: {
|
||||||
subjectId: account.id,
|
subjectId: account.id,
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!relationship) return errorResponse("Relationship not found", 404);
|
if (!relationship) return errorResponse("Relationship not found", 404);
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,63 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
import {
|
import {
|
||||||
checkForBidirectionalRelationships,
|
checkForBidirectionalRelationships,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
route: "/api/v1/follow_requests/:account_id/reject",
|
route: "/api/v1/follow_requests/:account_id/reject",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const { account_id } = matchedRoute.params;
|
const { account_id } = matchedRoute.params;
|
||||||
|
|
||||||
const account = await client.user.findUnique({
|
const account = await client.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: account_id,
|
id: account_id,
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!account) return errorResponse("Account not found", 404);
|
if (!account) return errorResponse("Account not found", 404);
|
||||||
|
|
||||||
// Check if there is a relationship on both sides
|
// Check if there is a relationship on both sides
|
||||||
await checkForBidirectionalRelationships(user, account);
|
await checkForBidirectionalRelationships(user, account);
|
||||||
|
|
||||||
// Reject follow request
|
// Reject follow request
|
||||||
await client.relationship.updateMany({
|
await client.relationship.updateMany({
|
||||||
where: {
|
where: {
|
||||||
subjectId: user.id,
|
subjectId: user.id,
|
||||||
ownerId: account.id,
|
ownerId: account.id,
|
||||||
requested: true,
|
requested: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
requested: false,
|
requested: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const relationship = await client.relationship.findFirst({
|
const relationship = await client.relationship.findFirst({
|
||||||
where: {
|
where: {
|
||||||
subjectId: account.id,
|
subjectId: account.id,
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!relationship) return errorResponse("Relationship not found", 404);
|
if (!relationship) return errorResponse("Relationship not found", 404);
|
||||||
|
|
||||||
return jsonResponse(relationshipToAPI(relationship));
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,73 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import { userToAPI } from "~database/entities/User";
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/follow_requests",
|
route: "/api/v1/follow_requests",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
max_id?: string;
|
max_id?: string;
|
||||||
since_id?: string;
|
since_id?: string;
|
||||||
min_id?: string;
|
min_id?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
||||||
|
|
||||||
if (limit < 1 || limit > 40) {
|
if (limit < 1 || limit > 40) {
|
||||||
return errorResponse("Limit must be between 1 and 40", 400);
|
return errorResponse("Limit must be between 1 and 40", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const objects = await client.user.findMany({
|
const objects = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
lt: max_id ?? undefined,
|
lt: max_id ?? undefined,
|
||||||
gte: since_id ?? undefined,
|
gte: since_id ?? undefined,
|
||||||
gt: min_id ?? undefined,
|
gt: min_id ?? undefined,
|
||||||
},
|
},
|
||||||
relationships: {
|
relationships: {
|
||||||
some: {
|
some: {
|
||||||
subjectId: user.id,
|
subjectId: user.id,
|
||||||
requested: true,
|
requested: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: "desc",
|
id: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Constuct HTTP Link header (next and prev)
|
// Constuct HTTP Link header (next and prev)
|
||||||
const linkHeader = [];
|
const linkHeader = [];
|
||||||
if (objects.length > 0) {
|
if (objects.length > 0) {
|
||||||
const urlWithoutQuery = req.url.split("?")[0];
|
const urlWithoutQuery = req.url.split("?")[0];
|
||||||
linkHeader.push(
|
linkHeader.push(
|
||||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
objects.map(user => userToAPI(user)),
|
objects.map((user) => userToAPI(user)),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: linkHeader.join(", "),
|
Link: linkHeader.join(", "),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,157 +2,157 @@ import { apiRoute, applyConfig } from "@api";
|
||||||
import { jsonResponse } from "@response";
|
import { jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
import { userToAPI } from "~database/entities/User";
|
import { userToAPI } from "~database/entities/User";
|
||||||
import type { APIInstance } from "~types/entities/instance";
|
|
||||||
import manifest from "~package.json";
|
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
import manifest from "~package.json";
|
||||||
|
import type { APIInstance } from "~types/entities/instance";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/instance",
|
route: "/api/v1/instance",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 300,
|
max: 300,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
|
|
||||||
// Get software version from package.json
|
// Get software version from package.json
|
||||||
const version = manifest.version;
|
const version = manifest.version;
|
||||||
|
|
||||||
const statusCount = await client.status.count({
|
const statusCount = await client.status.count({
|
||||||
where: {
|
where: {
|
||||||
instanceId: null,
|
instanceId: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const userCount = await client.user.count({
|
const userCount = await client.user.count({
|
||||||
where: {
|
where: {
|
||||||
instanceId: null,
|
instanceId: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the first created admin user
|
// Get the first created admin user
|
||||||
const contactAccount = await client.user.findFirst({
|
const contactAccount = await client.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
instanceId: null,
|
instanceId: null,
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: "asc",
|
id: "asc",
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get user that have posted once in the last 30 days
|
// Get user that have posted once in the last 30 days
|
||||||
const monthlyActiveUsers = await client.user.count({
|
const monthlyActiveUsers = await client.user.count({
|
||||||
where: {
|
where: {
|
||||||
instanceId: null,
|
instanceId: null,
|
||||||
statuses: {
|
statuses: {
|
||||||
some: {
|
some: {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const knownDomainsCount = await client.instance.count();
|
const knownDomainsCount = await client.instance.count();
|
||||||
|
|
||||||
// TODO: fill in more values
|
// TODO: fill in more values
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
approval_required: false,
|
approval_required: false,
|
||||||
configuration: {
|
configuration: {
|
||||||
media_attachments: {
|
media_attachments: {
|
||||||
image_matrix_limit: config.validation.max_media_attachments,
|
image_matrix_limit: config.validation.max_media_attachments,
|
||||||
image_size_limit: config.validation.max_media_size,
|
image_size_limit: config.validation.max_media_size,
|
||||||
supported_mime_types: config.validation.allowed_mime_types,
|
supported_mime_types: config.validation.allowed_mime_types,
|
||||||
video_frame_limit: 60,
|
video_frame_limit: 60,
|
||||||
video_matrix_limit: 10,
|
video_matrix_limit: 10,
|
||||||
video_size_limit: config.validation.max_media_size,
|
video_size_limit: config.validation.max_media_size,
|
||||||
},
|
},
|
||||||
polls: {
|
polls: {
|
||||||
max_characters_per_option:
|
max_characters_per_option:
|
||||||
config.validation.max_poll_option_size,
|
config.validation.max_poll_option_size,
|
||||||
max_expiration: config.validation.max_poll_duration,
|
max_expiration: config.validation.max_poll_duration,
|
||||||
max_options: config.validation.max_poll_options,
|
max_options: config.validation.max_poll_options,
|
||||||
min_expiration: 60,
|
min_expiration: 60,
|
||||||
},
|
},
|
||||||
statuses: {
|
statuses: {
|
||||||
characters_reserved_per_url: 0,
|
characters_reserved_per_url: 0,
|
||||||
max_characters: config.validation.max_note_size,
|
max_characters: config.validation.max_note_size,
|
||||||
max_media_attachments: config.validation.max_media_attachments,
|
max_media_attachments: config.validation.max_media_attachments,
|
||||||
supported_mime_types: [
|
supported_mime_types: [
|
||||||
"text/plain",
|
"text/plain",
|
||||||
"text/markdown",
|
"text/markdown",
|
||||||
"text/html",
|
"text/html",
|
||||||
"text/x.misskeymarkdown",
|
"text/x.misskeymarkdown",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: "A test instance",
|
description: "A test instance",
|
||||||
email: "",
|
email: "",
|
||||||
invites_enabled: false,
|
invites_enabled: false,
|
||||||
registrations: config.signups.registration,
|
registrations: config.signups.registration,
|
||||||
languages: ["en"],
|
languages: ["en"],
|
||||||
rules: config.signups.rules.map((r, index) => ({
|
rules: config.signups.rules.map((r, index) => ({
|
||||||
id: String(index),
|
id: String(index),
|
||||||
text: r,
|
text: r,
|
||||||
})),
|
})),
|
||||||
stats: {
|
stats: {
|
||||||
domain_count: knownDomainsCount,
|
domain_count: knownDomainsCount,
|
||||||
status_count: statusCount,
|
status_count: statusCount,
|
||||||
user_count: userCount,
|
user_count: userCount,
|
||||||
},
|
},
|
||||||
thumbnail: "",
|
thumbnail: "",
|
||||||
tos_url: config.signups.tos_url,
|
tos_url: config.signups.tos_url,
|
||||||
title: "Test Instance",
|
title: "Test Instance",
|
||||||
uri: new URL(config.http.base_url).hostname,
|
uri: new URL(config.http.base_url).hostname,
|
||||||
urls: {
|
urls: {
|
||||||
streaming_api: "",
|
streaming_api: "",
|
||||||
},
|
},
|
||||||
version: `4.2.0+glitch (compatible; Lysand ${version}})`,
|
version: `4.2.0+glitch (compatible; Lysand ${version}})`,
|
||||||
max_toot_chars: config.validation.max_note_size,
|
max_toot_chars: config.validation.max_note_size,
|
||||||
pleroma: {
|
pleroma: {
|
||||||
metadata: {
|
metadata: {
|
||||||
// account_activation_required: false,
|
// account_activation_required: false,
|
||||||
features: [
|
features: [
|
||||||
"pleroma_api",
|
"pleroma_api",
|
||||||
"akkoma_api",
|
"akkoma_api",
|
||||||
"mastodon_api",
|
"mastodon_api",
|
||||||
// "mastodon_api_streaming",
|
// "mastodon_api_streaming",
|
||||||
// "polls",
|
// "polls",
|
||||||
// "v2_suggestions",
|
// "v2_suggestions",
|
||||||
// "pleroma_explicit_addressing",
|
// "pleroma_explicit_addressing",
|
||||||
// "shareable_emoji_packs",
|
// "shareable_emoji_packs",
|
||||||
// "multifetch",
|
// "multifetch",
|
||||||
// "pleroma:api/v1/notifications:include_types_filter",
|
// "pleroma:api/v1/notifications:include_types_filter",
|
||||||
"quote_posting",
|
"quote_posting",
|
||||||
"editing",
|
"editing",
|
||||||
// "bubble_timeline",
|
// "bubble_timeline",
|
||||||
// "relay",
|
// "relay",
|
||||||
// "pleroma_emoji_reactions",
|
// "pleroma_emoji_reactions",
|
||||||
// "exposable_reactions",
|
// "exposable_reactions",
|
||||||
// "profile_directory",
|
// "profile_directory",
|
||||||
// "custom_emoji_reactions",
|
// "custom_emoji_reactions",
|
||||||
// "pleroma:get:main/ostatus",
|
// "pleroma:get:main/ostatus",
|
||||||
],
|
],
|
||||||
post_formats: [
|
post_formats: [
|
||||||
"text/plain",
|
"text/plain",
|
||||||
"text/html",
|
"text/html",
|
||||||
"text/markdown",
|
"text/markdown",
|
||||||
"text/x.misskeymarkdown",
|
"text/x.misskeymarkdown",
|
||||||
],
|
],
|
||||||
privileged_staff: false,
|
privileged_staff: false,
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
mau: monthlyActiveUsers,
|
mau: monthlyActiveUsers,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contact_account: contactAccount ? userToAPI(contactAccount) : null,
|
contact_account: contactAccount ? userToAPI(contactAccount) : null,
|
||||||
} as APIInstance);
|
} as APIInstance);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,108 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
|
||||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
|
||||||
import type { MediaBackend } from "media-manager";
|
import type { MediaBackend } from "media-manager";
|
||||||
import { MediaBackendType } from "media-manager";
|
import { MediaBackendType } from "media-manager";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||||
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
||||||
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET", "PUT"],
|
allowedMethods: ["GET", "PUT"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 10,
|
max: 10,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/api/v1/media/:id",
|
route: "/api/v1/media/:id",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:media"],
|
oauthPermissions: ["write:media"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get media information
|
* Get media information
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
thumbnail?: File;
|
thumbnail?: File;
|
||||||
description?: string;
|
description?: string;
|
||||||
focus?: string;
|
focus?: string;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return errorResponse("Unauthorized", 401);
|
return errorResponse("Unauthorized", 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = matchedRoute.params.id;
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
const attachment = await client.attachment.findUnique({
|
const attachment = await client.attachment.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
return errorResponse("Media not found", 404);
|
return errorResponse("Media not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
|
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
case "GET": {
|
case "GET": {
|
||||||
if (attachment.url) {
|
if (attachment.url) {
|
||||||
return jsonResponse(attachmentToAPI(attachment));
|
return jsonResponse(attachmentToAPI(attachment));
|
||||||
} else {
|
}
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 206,
|
status: 206,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
case "PUT": {
|
||||||
case "PUT": {
|
const { description, thumbnail } = extraData.parsedRequest;
|
||||||
const { description, thumbnail } = extraData.parsedRequest;
|
|
||||||
|
|
||||||
let thumbnailUrl = attachment.thumbnail_url;
|
let thumbnailUrl = attachment.thumbnail_url;
|
||||||
|
|
||||||
let mediaManager: MediaBackend;
|
let mediaManager: MediaBackend;
|
||||||
|
|
||||||
switch (config.media.backend as MediaBackendType) {
|
switch (config.media.backend as MediaBackendType) {
|
||||||
case MediaBackendType.LOCAL:
|
case MediaBackendType.LOCAL:
|
||||||
mediaManager = new LocalMediaBackend(config);
|
mediaManager = new LocalMediaBackend(config);
|
||||||
break;
|
break;
|
||||||
case MediaBackendType.S3:
|
case MediaBackendType.S3:
|
||||||
mediaManager = new S3MediaBackend(config);
|
mediaManager = new S3MediaBackend(config);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// TODO: Replace with logger
|
// TODO: Replace with logger
|
||||||
throw new Error("Invalid media backend");
|
throw new Error("Invalid media backend");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
||||||
thumbnailUrl = getUrl(uploadedFile.name, config);
|
thumbnailUrl = getUrl(uploadedFile.name, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
const descriptionText = description || attachment.description;
|
const descriptionText = description || attachment.description;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
descriptionText !== attachment.description ||
|
descriptionText !== attachment.description ||
|
||||||
thumbnailUrl !== attachment.thumbnail_url
|
thumbnailUrl !== attachment.thumbnail_url
|
||||||
) {
|
) {
|
||||||
const newAttachment = await client.attachment.update({
|
const newAttachment = await client.attachment.update({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
description: descriptionText,
|
description: descriptionText,
|
||||||
thumbnail_url: thumbnailUrl,
|
thumbnail_url: thumbnailUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(attachmentToAPI(newAttachment));
|
return jsonResponse(attachmentToAPI(newAttachment));
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(attachmentToAPI(attachment));
|
return jsonResponse(attachmentToAPI(attachment));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorResponse("Method not allowed", 405);
|
return errorResponse("Method not allowed", 405);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,136 +1,136 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
|
||||||
import { encode } from "blurhash";
|
import { encode } from "blurhash";
|
||||||
import sharp from "sharp";
|
|
||||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
|
||||||
import { MediaBackendType } from "media-manager";
|
import { MediaBackendType } from "media-manager";
|
||||||
import type { MediaBackend } from "media-manager";
|
import type { MediaBackend } from "media-manager";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||||
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
||||||
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 10,
|
max: 10,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
route: "/api/v1/media",
|
route: "/api/v1/media",
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
oauthPermissions: ["write:media"],
|
oauthPermissions: ["write:media"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload new media
|
* Upload new media
|
||||||
*/
|
*/
|
||||||
export default apiRoute<{
|
export default apiRoute<{
|
||||||
file: File;
|
file: File;
|
||||||
thumbnail?: File;
|
thumbnail?: File;
|
||||||
description?: string;
|
description?: string;
|
||||||
// TODO: Add focus
|
// TODO: Add focus
|
||||||
focus?: string;
|
focus?: string;
|
||||||
}>(async (req, matchedRoute, extraData) => {
|
}>(async (req, matchedRoute, extraData) => {
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return errorResponse("Unauthorized", 401);
|
return errorResponse("Unauthorized", 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { file, thumbnail, description } = extraData.parsedRequest;
|
const { file, thumbnail, description } = extraData.parsedRequest;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return errorResponse("No file provided", 400);
|
return errorResponse("No file provided", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
|
|
||||||
if (file.size > config.validation.max_media_size) {
|
if (file.size > config.validation.max_media_size) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`File too large, max size is ${config.validation.max_media_size} bytes`,
|
`File too large, max size is ${config.validation.max_media_size} bytes`,
|
||||||
413
|
413,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
config.validation.enforce_mime_types &&
|
config.validation.enforce_mime_types &&
|
||||||
!config.validation.allowed_mime_types.includes(file.type)
|
!config.validation.allowed_mime_types.includes(file.type)
|
||||||
) {
|
) {
|
||||||
return errorResponse("Invalid file type", 415);
|
return errorResponse("Invalid file type", 415);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
description &&
|
description &&
|
||||||
description.length > config.validation.max_media_description_size
|
description.length > config.validation.max_media_description_size
|
||||||
) {
|
) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`Description too long, max length is ${config.validation.max_media_description_size} characters`,
|
`Description too long, max length is ${config.validation.max_media_description_size} characters`,
|
||||||
413
|
413,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sha256 = new Bun.SHA256();
|
const sha256 = new Bun.SHA256();
|
||||||
|
|
||||||
const isImage = file.type.startsWith("image/");
|
const isImage = file.type.startsWith("image/");
|
||||||
|
|
||||||
const metadata = isImage
|
const metadata = isImage
|
||||||
? await sharp(await file.arrayBuffer()).metadata()
|
? await sharp(await file.arrayBuffer()).metadata()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const blurhash = isImage
|
const blurhash = isImage
|
||||||
? encode(
|
? encode(
|
||||||
new Uint8ClampedArray(await file.arrayBuffer()),
|
new Uint8ClampedArray(await file.arrayBuffer()),
|
||||||
metadata?.width ?? 0,
|
metadata?.width ?? 0,
|
||||||
metadata?.height ?? 0,
|
metadata?.height ?? 0,
|
||||||
4,
|
4,
|
||||||
4
|
4,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let url = "";
|
let url = "";
|
||||||
|
|
||||||
let mediaManager: MediaBackend;
|
let mediaManager: MediaBackend;
|
||||||
|
|
||||||
switch (config.media.backend as MediaBackendType) {
|
switch (config.media.backend as MediaBackendType) {
|
||||||
case MediaBackendType.LOCAL:
|
case MediaBackendType.LOCAL:
|
||||||
mediaManager = new LocalMediaBackend(config);
|
mediaManager = new LocalMediaBackend(config);
|
||||||
break;
|
break;
|
||||||
case MediaBackendType.S3:
|
case MediaBackendType.S3:
|
||||||
mediaManager = new S3MediaBackend(config);
|
mediaManager = new S3MediaBackend(config);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// TODO: Replace with logger
|
// TODO: Replace with logger
|
||||||
throw new Error("Invalid media backend");
|
throw new Error("Invalid media backend");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uploadedFile } = await mediaManager.addFile(file);
|
const { uploadedFile } = await mediaManager.addFile(file);
|
||||||
|
|
||||||
url = getUrl(uploadedFile.name, config);
|
url = getUrl(uploadedFile.name, config);
|
||||||
|
|
||||||
let thumbnailUrl = "";
|
let thumbnailUrl = "";
|
||||||
|
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
||||||
|
|
||||||
thumbnailUrl = getUrl(uploadedFile.name, config);
|
thumbnailUrl = getUrl(uploadedFile.name, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAttachment = await client.attachment.create({
|
const newAttachment = await client.attachment.create({
|
||||||
data: {
|
data: {
|
||||||
url,
|
url,
|
||||||
thumbnail_url: thumbnailUrl,
|
thumbnail_url: thumbnailUrl,
|
||||||
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
||||||
mime_type: file.type,
|
mime_type: file.type,
|
||||||
description: description ?? "",
|
description: description ?? "",
|
||||||
size: file.size,
|
size: file.size,
|
||||||
blurhash: blurhash ?? undefined,
|
blurhash: blurhash ?? undefined,
|
||||||
width: metadata?.width ?? undefined,
|
width: metadata?.width ?? undefined,
|
||||||
height: metadata?.height ?? undefined,
|
height: metadata?.height ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Add job to process videos and other media
|
// TODO: Add job to process videos and other media
|
||||||
|
|
||||||
return jsonResponse(attachmentToAPI(newAttachment));
|
return jsonResponse(attachmentToAPI(newAttachment));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
|
import { userToAPI } from "~database/entities/User";
|
||||||
import { userRelations } from "~database/entities/relations";
|
import { userRelations } from "~database/entities/relations";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
route: "/api/v1/mutes",
|
route: "/api/v1/mutes",
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: 100,
|
max: 100,
|
||||||
duration: 60,
|
duration: 60,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const blocks = await client.user.findMany({
|
const blocks = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
relationshipSubjects: {
|
relationshipSubjects: {
|
||||||
some: {
|
some: {
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
muting: true,
|
muting: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(blocks.map(u => userToAPI(u)));
|
return jsonResponse(blocks.map((u) => userToAPI(u)));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
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