From f4fd16179c8233d99a76a6a05d6cf885b4d35486 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 10 Mar 2024 19:30:26 -1000 Subject: [PATCH] Leave CLI as broken --- README.md | 24 +-- cli.ts | 127 +++++++++++++++- packages/cli-parser/cli-builder.type.ts | 19 ++- packages/cli-parser/index.ts | 77 +++++++--- packages/cli-parser/tests/cli-builder.test.ts | 139 ++++++++++++++---- server.ts | 41 ++++-- 6 files changed, 350 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 145fe01c..53a9f152 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,11 @@ ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) ![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white) ![VS Code Insiders](https://img.shields.io/badge/VS%20Code%20Insiders-35b393.svg?style=for-the-badge&logo=visual-studio-code&logoColor=white) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) ![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black) ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) ![ESLint](https://img.shields.io/badge/ESLint-4B3263?style=for-the-badge&logo=eslint&logoColor=white) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa?style=for-the-badge)](code_of_conduct.md) -> [!IMPORTANT] -> This project is **not abandoned**, my laptop merely broke and I am waiting for a new one to arrive - ## What is this? -This is a project to create a federated social network based on the [Lysand](https://lysand.org) protocol. It is currently in alpha phase, with basic federation and API support. +This is a project to create a federated social network based on the [Lysand](https://lysand.org) protocol. It is currently in alpha phase, with basic federation and almost complete Mastodon API support. -This project aims to be a fully featured social network, with a focus on privacy, security, and performance. It will implement the Mastodon API for support with clients that already support Mastodon or Pleroma. +This project aims to be a fully featured social network, with a focus on privacy, security, and performance. It implements the Mastodon API for support with clients that already support Mastodon or Pleroma. > [!NOTE] > This project is not affiliated with Mastodon or Pleroma, and is not a fork of either project. It is a new project built from the ground up. @@ -35,7 +32,7 @@ This project aims to be a fully featured social network, with a focus on privacy ## Benchmarks > [!NOTE] -> These benchmarks are not representative of real-world performance, and are only meant to be used as a rough guide. +> These benchmarks are not representative of real-world performance, and are only meant to be used as a rough guide. Load, and therefore performance, will vary depending on the server's hardware and software configuration, as well as user activity. ### Timeline Benchmarks @@ -67,18 +64,21 @@ $ bun run benchmarks/timelines.ts 10000 ✓ 10000 requests fulfilled in 12.44852s ``` -Lysand is extremely fast and can handle tens of thousands of HTTP requests per second on a good server. +Lysand is extremely fast and can handle thousands of HTTP requests per second on a good server. ## How do I run it? ### Requirements -- The [Bun Runtime](https://bun.sh), version 1.0.5 or later (usage of the latest version is recommended) +- The [Bun Runtime](https://bun.sh), version 1.0.30 or later (usage of the latest version is recommended) - A PostgreSQL database - (Optional but recommended) A Linux-based operating system - (Optional if you want search) A working Meiliseach instance -> **Note**: We will not be offerring support to Windows or MacOS users. If you are using one of these operating systems, please use a virtual machine or container to run Lysand. +> [!WARNING] +> Lysand has not been tested on Windows or MacOS. It is recommended to use a Linux-based operating system to run Lysand. +> +> We will not be offerring support to Windows or MacOS users. If you are using one of these operating systems, please use a virtual machine or container to run Lysand. ### Installation @@ -152,6 +152,9 @@ bun start ### Using the CLI +> [!WARNING] +> The CLI is currently broken due to unknown bugs that are actively being investigated. The following instructions are for when this is fixed. + Lysand includes a built-in CLI for managing the server. To use it, simply run the following command: ```bash @@ -279,10 +282,12 @@ Working endpoints are: - `/api/v1/blocks` - `/api/v1/mutes` - `/api/v2/media` +- `/api/v1/notifications` Tests needed but completed: - `/api/v1/media/:id` +- `/api/v2/media` - `/api/v1/favourites` - `/api/v1/accounts/:id/followers` - `/api/v1/accounts/:id/following` @@ -335,7 +340,6 @@ Endpoints left: - `/api/v1/lists/:id` (`GET`, `PUT`, `DELETE`) - `/api/v1/markers` (`GET`, `POST`) - `/api/v1/lists/:id/accounts` (`GET`, `POST`, `DELETE`) -- `/api/v1/notifications` - `/api/v1/notifications/:id` - `/api/v1/notifications/clear` - `/api/v1/notifications/:id/dismiss` diff --git a/cli.ts b/cli.ts index c94c14d7..1392520e 100644 --- a/cli.ts +++ b/cli.ts @@ -1,23 +1,139 @@ -import type { Prisma } from "@prisma/client"; import chalk from "chalk"; -import { client } from "~database/datasource"; import { createNewLocalUser } from "~database/entities/User"; import Table from "cli-table"; import { rebuildSearchIndexes, MeiliIndexType } from "@meilisearch"; -import { getConfig } from "~classes/configmanager"; -import { uploadFile } from "~classes/media"; import { getUrl } from "~database/entities/Attachment"; import { mkdir, exists } from "fs/promises"; import extract from "extract-zip"; +import { client } from "~database/datasource"; +import { CliBuilder, CliCommand } from "cli-parser"; +import { CliParameterType } from "~packages/cli-parser/cli-builder.type"; +import { PrismaClient } from "@prisma/client"; +import { ConfigManager } from "~packages/config-manager"; const args = process.argv; +const config = await new ConfigManager({}).getConfig(); + +console.error("CLI is temporarily broken, please use the Prisma CLI instead"); +process.exit(1); + +const cliBuilder = new CliBuilder([ + new CliCommand( + ["help"], + [], + () => { + cliBuilder.displayHelp(); + }, + "Shows help for the CLI" + ), + new CliCommand<{ + username: string; + password: string; + email: string; + admin: boolean; + help: boolean; + }>( + ["user", "create"], + [ + { + name: "username", + type: CliParameterType.STRING, + description: "Username of the user", + needsValue: true, + positioned: false, + }, + { + name: "password", + type: CliParameterType.STRING, + description: "Password of the user", + needsValue: true, + positioned: false, + }, + { + name: "email", + type: CliParameterType.STRING, + description: "Email of the user", + needsValue: true, + positioned: false, + }, + { + name: "admin", + type: CliParameterType.BOOLEAN, + description: "Make the user an admin", + needsValue: false, + positioned: false, + }, + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + }, + ], + (instance: CliCommand, args) => { + const { username, password, email, admin, help } = args; + + if (help) { + instance.displayHelp(); + return; + } + + // Check if username, password and email are provided + if (!username || !password || !email) { + console.log( + `${chalk.red(`✗`)} Missing username, password or email` + ); + return; + } + + // Check if user already exists + void client.user + .findFirst({ + where: { + OR: [{ username }, { email }], + }, + }) + .then(user => { + if (user) { + console.log(`${chalk.red(`✗`)} User already exists`); + return; + } + + console.log("Sus"); + + // Create user + /* const newUser = await createNewLocalUser({ + email: email, + password: password, + username: username, + admin: admin, + }); + + console.log( + `${chalk.green(`✓`)} Created user ${chalk.blue( + newUser.username + )}${admin ? chalk.green(" (admin)") : ""}` + ); */ + }); + }, + "Creates a new user", + "bun cli user create --username admin --password password123 --email email@email.com" + ), +]); + +cliBuilder.processArgs(args); + +process.exit(0); + /** * Make the text have a width of 20 characters, padding with gray dots * Text can be a Chalk string, in which case formatting codes should not be counted in text length * @param text The text to align */ -const alignDots = (text: string, length = 20) => { +/* const alignDots = (text: string, length = 20) => { // Remove formatting codes // eslint-disable-next-line no-control-regex const textLength = text.replace(/\u001b\[\d+m/g, "").length; @@ -1065,3 +1181,4 @@ switch (command) { } process.exit(0); + */ diff --git a/packages/cli-parser/cli-builder.type.ts b/packages/cli-parser/cli-builder.type.ts index f1f7b2c2..89c6dece 100644 --- a/packages/cli-parser/cli-builder.type.ts +++ b/packages/cli-parser/cli-builder.type.ts @@ -1,10 +1,23 @@ export interface CliParameter { name: string; - // If not positioned, the argument will need to be called with --name value instead of just value + /* Like -v for --version */ + shortName?: string; + /** + * If not positioned, the argument will need to be called with --name value instead of just value + * @default true + */ 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; optional?: true; - type: "string" | "number" | "boolean" | "array"; + type: CliParameterType; description?: string; } + +export enum CliParameterType { + STRING = "string", + NUMBER = "number", + BOOLEAN = "boolean", + ARRAY = "array", + EMPTY = "empty", +} diff --git a/packages/cli-parser/index.ts b/packages/cli-parser/index.ts index d9228374..2ce9539c 100644 --- a/packages/cli-parser/index.ts +++ b/packages/cli-parser/index.ts @@ -1,4 +1,4 @@ -import type { CliParameter } from "./cli-builder.type"; +import { CliParameterType, type CliParameter } from "./cli-builder.type"; import chalk from "chalk"; export function startsWithArray(fullArray: any[], startArray: any[]) { @@ -193,21 +193,21 @@ export class CliBuilder { if (value instanceof CliCommand) { writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}|${chalk.underline(value.description)}\n`; const positionedArgs = value.argTypes.filter( - arg => arg.positioned + arg => arg.positioned ?? true ); const unpositionedArgs = value.argTypes.filter( - arg => !arg.positioned + arg => !(arg.positioned ?? true) ); - for (const arg of unpositionedArgs) { + for (const arg of positionedArgs) { writeBuffer += `${" ".repeat(depth + 1)}${chalk.green( arg.name )}|${ arg.description ?? "(no description)" } ${arg.optional ? chalk.gray("(optional)") : ""}\n`; } - for (const arg of positionedArgs) { - writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}|${ + for (const arg of unpositionedArgs) { + writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}${arg.shortName ? ", " + chalk.yellow("-" + arg.shortName) : ""}|${ arg.description ?? "(no description)" } ${arg.optional ? chalk.gray("(optional)") : ""}\n`; } @@ -253,15 +253,38 @@ export class CliBuilder { } } +/* type CliParametersToType = { + [K in T[number]["name"]]: T[number]["type"] extends CliParameterType.STRING + ? string + : T[number]["type"] extends CliParameterType.NUMBER + ? number + : T[number]["type"] extends CliParameterType.BOOLEAN + ? boolean + : T[number]["type"] extends CliParameterType.ARRAY + ? string[] + : T[number]["type"] extends CliParameterType.EMPTY + ? never + : never; +}; + +type ExecuteFunction = ( + args: CliParametersToType +) => void; */ + +type ExecuteFunction = ( + instance: CliCommand, + args: Partial +) => Promise | void; + /** * A command that can be executed from the command line * @param categories Example: `["user", "create"]` for the command `./cli user create --name John` */ -export class CliCommand { +export class CliCommand { constructor( public categories: string[], public argTypes: CliParameter[], - private execute: (args: Record) => void, + private execute: ExecuteFunction, public description?: string, public example?: string ) {} @@ -271,13 +294,17 @@ export class CliCommand { * formatted with Chalk and with emojis */ displayHelp() { - const positionedArgs = this.argTypes.filter(arg => arg.positioned); - const unpositionedArgs = this.argTypes.filter(arg => !arg.positioned); + 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(" "))} ${this.description ? `${chalk.cyan(this.description)}\n` : ""} ${chalk.magenta("🔧 Arguments:")} -${unpositionedArgs +${positionedArgs .map( arg => `${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${ @@ -285,10 +312,10 @@ ${unpositionedArgs }` ) .join("\n")} -${positionedArgs +${unpositionedArgs .map( arg => - `--${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${ + `--${chalk.bold(arg.name)}${arg.shortName ? `, -${arg.shortName}` : ""}: ${chalk.blue(arg.description ?? "(no description)")} ${ arg.optional ? chalk.gray("(optional)") : "" }` ) @@ -328,6 +355,20 @@ ${positionedArgs i++; currentParameter = null; } + } else if (arg.startsWith("-")) { + const shortName = arg.substring(1); + const argType = this.argTypes.find( + argType => argType.shortName === shortName + ); + if (argType && !argType.needsValue) { + parsedArgs[argType.name] = true; + } else if (argType && argType.needsValue) { + parsedArgs[argType.name] = this.castArgValue( + argsWithoutCategories[i + 1], + argType.type + ); + i++; + } } else if (currentParameter) { parsedArgs[currentParameter.name] = this.castArgValue( arg, @@ -352,13 +393,13 @@ ${positionedArgs private castArgValue(value: string, type: CliParameter["type"]): any { switch (type) { - case "string": + case CliParameterType.STRING: return value; - case "number": + case CliParameterType.NUMBER: return Number(value); - case "boolean": + case CliParameterType.BOOLEAN: return value === "true"; - case "array": + case CliParameterType.ARRAY: return value.split(","); default: return value; @@ -370,6 +411,6 @@ ${positionedArgs */ run(argsWithoutCategories: string[]) { const args = this.parseArgs(argsWithoutCategories); - this.execute(args); + void this.execute(this, args as any); } } diff --git a/packages/cli-parser/tests/cli-builder.test.ts b/packages/cli-parser/tests/cli-builder.test.ts index 6a4b12de..75b1a41b 100644 --- a/packages/cli-parser/tests/cli-builder.test.ts +++ b/packages/cli-parser/tests/cli-builder.test.ts @@ -2,6 +2,7 @@ import { CliCommand, CliBuilder, startsWithArray } from ".."; import { describe, beforeEach, it, expect, jest, spyOn } from "bun:test"; import stripAnsi from "strip-ansi"; +import { CliParameterType } from "../cli-builder.type"; describe("startsWithArray", () => { it("should return true when fullArray starts with startArray", () => { @@ -36,10 +37,27 @@ describe("CliCommand", () => { cliCommand = new CliCommand( ["category1", "category2"], [ - { name: "arg1", type: "string", needsValue: true }, - { name: "arg2", type: "number", needsValue: true }, - { name: "arg3", type: "boolean", needsValue: false }, - { name: "arg4", type: "array", needsValue: true }, + { + name: "arg1", + type: CliParameterType.STRING, + needsValue: true, + }, + { + name: "arg2", + shortName: "a", + type: CliParameterType.NUMBER, + needsValue: true, + }, + { + name: "arg3", + type: CliParameterType.BOOLEAN, + needsValue: false, + }, + { + name: "arg4", + type: CliParameterType.ARRAY, + needsValue: true, + }, ], () => { // Do nothing @@ -65,13 +83,34 @@ describe("CliCommand", () => { }); }); - it("should cast argument values correctly", () => { - expect(cliCommand["castArgValue"]("42", "number")).toBe(42); - expect(cliCommand["castArgValue"]("true", "boolean")).toBe(true); - expect(cliCommand["castArgValue"]("value1,value2", "array")).toEqual([ + it("should parse short names for arguments too", () => { + const args = cliCommand["parseArgs"]([ + "--arg1", "value1", - "value2", + "-a", + "42", + "--arg3", + "--arg4", + "value1,value2", ]); + expect(args).toEqual({ + arg1: "value1", + arg2: 42, + arg3: true, + arg4: ["value1", "value2"], + }); + }); + + it("should cast argument values correctly", () => { + expect(cliCommand["castArgValue"]("42", CliParameterType.NUMBER)).toBe( + 42 + ); + expect( + cliCommand["castArgValue"]("true", CliParameterType.BOOLEAN) + ).toBe(true); + expect( + cliCommand["castArgValue"]("value1,value2", CliParameterType.ARRAY) + ).toEqual(["value1", "value2"]); }); it("should run the execute function with the parsed parameters", () => { @@ -79,10 +118,26 @@ describe("CliCommand", () => { cliCommand = new CliCommand( ["category1", "category2"], [ - { name: "arg1", type: "string", needsValue: true }, - { name: "arg2", type: "number", needsValue: true }, - { name: "arg3", type: "boolean", needsValue: false }, - { name: "arg4", type: "array", needsValue: true }, + { + name: "arg1", + type: CliParameterType.STRING, + needsValue: true, + }, + { + name: "arg2", + type: CliParameterType.NUMBER, + needsValue: true, + }, + { + name: "arg3", + type: CliParameterType.BOOLEAN, + needsValue: false, + }, + { + name: "arg4", + type: CliParameterType.ARRAY, + needsValue: true, + }, ], mockExecute ); @@ -109,13 +164,29 @@ describe("CliCommand", () => { cliCommand = new CliCommand( ["category1", "category2"], [ - { name: "arg1", type: "string", needsValue: true }, - { name: "arg2", type: "number", needsValue: true }, - { name: "arg3", type: "boolean", needsValue: false }, - { name: "arg4", type: "array", needsValue: true }, + { + name: "arg1", + type: CliParameterType.STRING, + needsValue: true, + }, + { + name: "arg2", + type: CliParameterType.NUMBER, + needsValue: true, + }, + { + name: "arg3", + type: CliParameterType.BOOLEAN, + needsValue: false, + }, + { + name: "arg4", + type: CliParameterType.ARRAY, + needsValue: true, + }, { name: "arg5", - type: "string", + type: CliParameterType.STRING, needsValue: true, positioned: true, }, @@ -153,31 +224,31 @@ describe("CliCommand", () => { [ { name: "arg1", - type: "string", + type: CliParameterType.STRING, needsValue: true, description: "Argument 1", optional: true, }, { name: "arg2", - type: "number", + type: CliParameterType.NUMBER, needsValue: true, description: "Argument 2", }, { name: "arg3", - type: "boolean", + type: CliParameterType.BOOLEAN, needsValue: false, description: "Argument 3", optional: true, - positioned: true, + positioned: false, }, { name: "arg4", - type: "array", + type: CliParameterType.ARRAY, needsValue: true, description: "Argument 4", - positioned: true, + positioned: false, }, ], () => { @@ -260,7 +331,7 @@ describe("CliBuilder", () => { [ { name: "arg1", - type: "string", + type: CliParameterType.STRING, needsValue: true, positioned: false, }, @@ -352,20 +423,28 @@ describe("CliBuilder", () => { [ { name: "name", - type: "string", + type: CliParameterType.STRING, needsValue: true, description: "Name of new item", }, { name: "delete-previous", - type: "number", + type: CliParameterType.NUMBER, needsValue: false, - positioned: true, + positioned: false, optional: true, description: "Also delete the previous item", }, - { name: "arg3", type: "boolean", needsValue: false }, - { name: "arg4", type: "array", needsValue: true }, + { + name: "arg3", + type: CliParameterType.BOOLEAN, + needsValue: false, + }, + { + name: "arg4", + type: CliParameterType.ARRAY, + needsValue: true, + }, ], () => { // Do nothing diff --git a/server.ts b/server.ts index 814a577e..4189f216 100644 --- a/server.ts +++ b/server.ts @@ -75,7 +75,11 @@ export const createServer = ( return errorResponse("Route not found", 500); } - if (matchedRoute && file != undefined) { + if ( + matchedRoute && + matchedRoute.name !== "/[...404]" && + file != undefined + ) { const meta = file.meta; // Check for allowed requests @@ -133,35 +137,50 @@ export const createServer = ( configManager, parsedRequest, }); - } else { + } else if (matchedRoute?.name === "/[...404]") { // Proxy response from Vite at localhost:5173 if in development mode if (isProd) { if (new URL(req.url).pathname.startsWith("/assets")) { - // Serve from pages/dist/assets - return new Response( - Bun.file(`./pages/dist${new URL(req.url).pathname}`) + const file = Bun.file( + `./pages/dist${new URL(req.url).pathname}` ); + + // Serve from pages/dist/assets + if (await file.exists()) { + return new Response(file); + } else return errorResponse("Asset not found", 404); + } + if (new URL(req.url).pathname.startsWith("/api")) { + return errorResponse("Route not found", 404); } + const file = Bun.file(`./pages/dist/index.html`); + // Serve from pages/dist - return new Response(Bun.file(`./pages/dist/index.html`)); + return new Response(file); } else { const proxy = await fetch( req.url.replace( config.http.base_url, "http://localhost:5173" ) - ); + ).catch(async e => { + await logger.logError( + LogLevel.ERROR, + "Server.Proxy", + e as Error + ); + return errorResponse("Route not found", 404); + }); if (proxy.status !== 404) { return proxy; } } - return new Response(undefined, { - status: 404, - statusText: "Route not found", - }); + return errorResponse("Route not found", 404); + } else { + return errorResponse("Route not found", 404); } }, });