From 28c73bc62adae05225c8df7c82669f6175cd3414 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 11 Mar 2024 20:20:38 -1000 Subject: [PATCH] Full CLI rework and repair --- .eslintrc.cjs | 2 +- README.md | 9 +- bun.lockb | Bin 428088 -> 429200 bytes cli.ts | 2757 ++++++++++------- package.json | 13 +- packages/cli-parser/index.ts | 9 +- packages/cli-parser/tests/cli-builder.test.ts | 12 +- packages/media-manager/index.ts | 18 + .../tests/media-backends.test.ts | 33 + 9 files changed, 1739 insertions(+), 1114 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 4e88644a..fe8ec85f 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -9,7 +9,7 @@ module.exports = { parserOptions: { project: "./tsconfig.json", }, - ignorePatterns: ["node_modules/", "dist/", ".eslintrc.cjs", "cli.ts"], + ignorePatterns: ["node_modules/", "dist/", ".eslintrc.cjs"], plugins: ["@typescript-eslint"], root: true, rules: { diff --git a/README.md b/README.md index 53a9f152..83a9e54d 100644 --- a/README.md +++ b/README.md @@ -152,22 +152,19 @@ 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 -bun cli +bun cli help ``` If you are running a production build, you will need to run `bun run dist/cli.js` or `./entrypoint.sh cli` instead. -You can use the `help` command to see a list of available commands. These include creating users, deleting users and more. +You can use the `help` command to see a list of available commands. These include creating users, deleting users and more. Each command also has a `--help,-h` flag that you can use to see more information about the command. #### Scripting with the CLI -Some CLI commands that return data as tables can be used in scripts. To do so, you can use the `--json` flag to output the data as JSON instead of a table, or even `--csv` to output the data as CSV. See `bun cli help` for more information. +Some CLI commands that return data as tables can be used in scripts. To convert them to JSON or CSV, some commands allow you to specify a `--format` flag that can be either `"json"` or `"csv"`. See `bun cli help` or `bun cli -h` for more information. Flags can be used in any order and anywhere in the script (except for the `bun cli` command itself). The command arguments themselves must be in the correct order, however. diff --git a/bun.lockb b/bun.lockb index 1c98ae20542afda925c7753cfa1c3883db63b458..f2a6bae957c4f6f7902af62acd6aefd4ace1c4ef 100755 GIT binary patch delta 80567 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 delta 79763 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 diff --git a/cli.ts b/cli.ts index 1392520e..f850b7d7 100644 --- a/cli.ts +++ b/cli.ts @@ -3,21 +3,22 @@ import { createNewLocalUser } from "~database/entities/User"; import Table from "cli-table"; import { rebuildSearchIndexes, MeiliIndexType } from "@meilisearch"; 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"; +import { Parser } from "@json2csv/plainjs"; +import type { Prisma } from "@prisma/client"; +import { MediaBackend } from "media-manager"; +import { mkdtemp } from "fs/promises"; +import { join } from "path"; +import { tmpdir } from "os"; 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"], @@ -73,12 +74,12 @@ const cliBuilder = new CliBuilder([ positioned: false, }, ], - (instance: CliCommand, args) => { + async (instance: CliCommand, args) => { const { username, password, email, admin, help } = args; if (help) { instance.displayHelp(); - return; + return 0; } // Check if username, password and email are provided @@ -86,1099 +87,1673 @@ const cliBuilder = new CliBuilder([ console.log( `${chalk.red(`✗`)} Missing username, password or email` ); - return; + return 1; } // 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, - }); + const user = await client.user.findFirst({ + where: { + OR: [{ username }, { email }], + }, + }); + if (user) { + if (user.username === username) { console.log( - `${chalk.green(`✓`)} Created user ${chalk.blue( - newUser.username - )}${admin ? chalk.green(" (admin)") : ""}` - ); */ - }); + `${chalk.red(`✗`)} User with username ${chalk.blue(username)} already exists` + ); + } else { + console.log( + `${chalk.red(`✗`)} User with email ${chalk.blue(email)} already exists` + ); + } + return 1; + } + + // 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)") : ""}` + ); + + return 0; }, "Creates a new user", "bun cli user create --username admin --password password123 --email email@email.com" ), + new CliCommand<{ + username: string; + help: boolean; + noconfirm: boolean; + }>( + ["user", "delete"], + [ + { + name: "username", + type: CliParameterType.STRING, + description: "Username of the user", + needsValue: true, + positioned: true, + }, + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + }, + { + name: "noconfirm", + shortName: "y", + type: CliParameterType.EMPTY, + description: "Skip confirmation", + needsValue: false, + positioned: false, + }, + ], + async (instance: CliCommand, args) => { + const { username, help } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (!username) { + console.log(`${chalk.red(`✗`)} Missing username`); + return 1; + } + + const user = await client.user.findFirst({ + where: { + username: username, + }, + }); + + if (!user) { + console.log(`${chalk.red(`✗`)} User not found`); + return 1; + } + + if (!args.noconfirm) { + process.stdout.write( + `Are you sure you want to delete user ${chalk.blue( + user.username + )}?\n${chalk.red(chalk.bold("This is a destructive action and cannot be undone!"))} [y/N] ` + ); + + for await (const line of console) { + if (line.trim().toLowerCase() === "y") { + break; + } else { + console.log(`${chalk.red(`✗`)} Deletion cancelled`); + return 0; + } + } + } + + await client.user.delete({ + where: { + id: user.id, + }, + }); + + console.log( + `${chalk.green(`✓`)} Deleted user ${chalk.blue(user.username)}` + ); + + return 0; + }, + "Deletes a user", + "bun cli user delete --username admin" + ), + new CliCommand<{ + admins: boolean; + help: boolean; + format: string; + limit: number; + redact: boolean; + fields: string[]; + }>( + ["user", "list"], + [ + { + name: "admins", + type: CliParameterType.BOOLEAN, + description: "List only admins", + needsValue: false, + positioned: false, + }, + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + }, + { + name: "format", + type: CliParameterType.STRING, + description: "Output format (can be json or csv)", + needsValue: true, + positioned: false, + optional: true, + }, + { + name: "limit", + type: CliParameterType.NUMBER, + description: + "Limit the number of users to list (defaults to 200)", + needsValue: true, + positioned: false, + optional: true, + }, + { + name: "redact", + type: CliParameterType.BOOLEAN, + description: + "Redact sensitive information (such as password hashes, emails or keys)", + needsValue: false, + positioned: false, + optional: true, + }, + { + name: "fields", + type: CliParameterType.ARRAY, + description: + "If provided, restricts output to these fields (comma-separated)", + needsValue: true, + positioned: false, + optional: true, + }, + ], + async (instance: CliCommand, args) => { + const { admins, help } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (args.format && !["json", "csv"].includes(args.format)) { + console.log(`${chalk.red(`✗`)} Invalid format`); + return 1; + } + + const users = await client.user.findMany({ + where: { + isAdmin: admins || undefined, + }, + take: args.limit ?? 200, + include: { + instance: true, + }, + }); + + if (args.redact) { + for (const user of users) { + user.email = "[REDACTED]"; + user.password = "[REDACTED]"; + user.publicKey = "[REDACTED]"; + user.privateKey = "[REDACTED]"; + } + } + + if (args.fields) { + for (const user of users) { + const keys = Object.keys(user); + for (const key of keys) { + if (!args.fields.includes(key)) { + // @ts-expect-error Shouldn't cause issues in this case + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete user[key]; + } + } + } + } + + if (args.format === "json") { + console.log(JSON.stringify(users, null, 4)); + return 0; + } else if (args.format == "csv") { + const parser = new Parser({}); + console.log(parser.parse(users)); + return 0; + } + + console.log( + `${chalk.green(`✓`)} Found ${chalk.blue(users.length)} users (limit ${args.limit ?? 200})` + ); + + const tableHead = { + username: chalk.white(chalk.bold("Username")), + email: chalk.white(chalk.bold("Email")), + displayName: chalk.white(chalk.bold("Display Name")), + isAdmin: chalk.white(chalk.bold("Admin?")), + instance: chalk.white(chalk.bold("Instance URL")), + createdAt: chalk.white(chalk.bold("Created At")), + id: chalk.white(chalk.bold("Internal UUID")), + }; + + // Only keep the fields specified if --fields is provided + if (args.fields) { + const keys = Object.keys(tableHead); + for (const key of keys) { + if (!args.fields.includes(key)) { + // @ts-expect-error This is fine + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete tableHead[key]; + } + } + } + + const table = new Table({ + head: Object.values(tableHead), + }); + + for (const user of users) { + // Print table of users + const data = { + username: () => chalk.yellow(`@${user.username}`), + email: () => chalk.green(user.email), + displayName: () => chalk.blue(user.displayName), + isAdmin: () => chalk.red(user.isAdmin ? "Yes" : "No"), + instance: () => + chalk.blue( + user.instance ? user.instance.base_url : "Local" + ), + createdAt: () => chalk.blue(user.createdAt.toISOString()), + id: () => chalk.blue(user.id), + }; + + // Only keep the fields specified if --fields is provided + if (args.fields) { + const keys = Object.keys(data); + for (const key of keys) { + if (!args.fields.includes(key)) { + // @ts-expect-error This is fine + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete data[key]; + } + } + } + + table.push(Object.values(data).map(fn => fn())); + } + + console.log(table.toString()); + + return 0; + }, + "Lists all users", + "bun cli user list" + ), + new CliCommand<{ + query: string; + fields: string[]; + format: string; + help: boolean; + "case-sensitive": boolean; + limit: number; + redact: boolean; + }>( + ["user", "search"], + [ + { + name: "query", + type: CliParameterType.STRING, + description: "Query to search for", + needsValue: true, + positioned: true, + }, + { + name: "fields", + type: CliParameterType.ARRAY, + description: "Fields to search in", + needsValue: true, + positioned: false, + }, + { + name: "format", + type: CliParameterType.STRING, + description: "Output format (can be json or csv)", + needsValue: true, + positioned: false, + optional: true, + }, + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + }, + { + name: "case-sensitive", + shortName: "c", + type: CliParameterType.EMPTY, + description: "Case-sensitive search", + needsValue: false, + positioned: false, + optional: true, + }, + { + name: "limit", + type: CliParameterType.NUMBER, + description: "Limit the number of users to list (default 20)", + needsValue: true, + positioned: false, + optional: true, + }, + { + name: "redact", + type: CliParameterType.BOOLEAN, + description: + "Redact sensitive information (such as password hashes, emails or keys)", + needsValue: false, + positioned: false, + optional: true, + }, + ], + async (instance: CliCommand, args) => { + const { + query, + fields = [], + help, + limit = 20, + "case-sensitive": caseSensitive = false, + redact, + } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (!query) { + console.log(`${chalk.red(`✗`)} Missing query parameter`); + return 1; + } + + if (fields.length === 0) { + console.log(`${chalk.red(`✗`)} Missing fields parameter`); + return 1; + } + + const queries: Prisma.UserWhereInput[] = []; + + for (const field of fields) { + queries.push({ + [field]: { + contains: query, + mode: caseSensitive ? "default" : "insensitive", + }, + }); + } + + const users = await client.user.findMany({ + where: { + OR: queries, + }, + include: { + instance: true, + }, + take: limit, + }); + + if (redact) { + for (const user of users) { + user.email = "[REDACTED]"; + user.password = "[REDACTED]"; + user.publicKey = "[REDACTED]"; + user.privateKey = "[REDACTED]"; + } + } + + if (args.format === "json") { + console.log(JSON.stringify(users, null, 4)); + return 0; + } else if (args.format === "csv") { + const parser = new Parser({}); + console.log(parser.parse(users)); + return 0; + } + + console.log( + `${chalk.green(`✓`)} Found ${chalk.blue(users.length)} users (limit ${limit})` + ); + + const table = new Table({ + head: [ + chalk.white(chalk.bold("Username")), + chalk.white(chalk.bold("Email")), + chalk.white(chalk.bold("Display Name")), + chalk.white(chalk.bold("Admin?")), + chalk.white(chalk.bold("Instance URL")), + ], + }); + + for (const user of users) { + table.push([ + chalk.yellow(`@${user.username}`), + chalk.green(user.email), + chalk.blue(user.displayName), + chalk.red(user.isAdmin ? "Yes" : "No"), + chalk.blue( + user.instanceId ? user.instance?.base_url : "Local" + ), + ]); + } + + console.log(table.toString()); + + return 0; + }, + "Searches for a user", + "bun cli user search bob --fields email,username" + ), + + new CliCommand<{ + username: string; + "issuer-id": string; + "server-id": string; + help: boolean; + }>( + ["user", "oidc", "connect"], + [ + { + name: "username", + type: CliParameterType.STRING, + description: "Username of the local account", + needsValue: true, + positioned: true, + }, + { + name: "issuer-id", + type: CliParameterType.STRING, + description: "ID of the OpenID Connect issuer in config", + needsValue: true, + positioned: false, + }, + { + name: "server-id", + type: CliParameterType.STRING, + description: "ID of the user on the OpenID Connect server", + needsValue: true, + positioned: false, + }, + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + }, + ], + async (instance: CliCommand, args) => { + const { + username, + "issuer-id": issuerId, + "server-id": serverId, + help, + } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (!username || !issuerId || !serverId) { + console.log(`${chalk.red(`✗`)} Missing username, issuer or ID`); + return 1; + } + + // Check if issuerId is valid + if (!config.oidc.providers.find(p => p.id === issuerId)) { + console.log(`${chalk.red(`✗`)} Invalid issuer ID`); + return 1; + } + + const user = await client.user.findFirst({ + where: { + username: username, + }, + include: { + linkedOpenIdAccounts: true, + }, + }); + + if (!user) { + console.log(`${chalk.red(`✗`)} User not found`); + return 1; + } + + if (user.linkedOpenIdAccounts.find(a => a.issuerId === issuerId)) { + console.log( + `${chalk.red(`✗`)} User ${chalk.blue( + user.username + )} is already connected to this OpenID Connect issuer with another account` + ); + return 1; + } + + // Connect the OpenID account + await client.user.update({ + where: { + id: user.id, + }, + data: { + linkedOpenIdAccounts: { + create: { + issuerId: issuerId, + serverId: serverId, + }, + }, + }, + }); + + console.log( + `${chalk.green(`✓`)} Connected OpenID Connect account to user ${chalk.blue( + user.username + )}` + ); + + return 0; + }, + "Connects an OpenID Connect account to a local account", + "bun cli user oidc connect admin google 123456789" + ), + new CliCommand<{ + "server-id": string; + help: boolean; + }>( + ["user", "oidc", "disconnect"], + [ + { + name: "server-id", + type: CliParameterType.STRING, + description: "Server ID of the OpenID Connect account", + needsValue: true, + positioned: true, + }, + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + }, + ], + async (instance: CliCommand, args) => { + const { "server-id": id, help } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (!id) { + console.log(`${chalk.red(`✗`)} Missing ID`); + return 1; + } + + const account = await client.openIdAccount.findFirst({ + where: { + serverId: id, + }, + include: { + User: true, + }, + }); + + if (!account) { + console.log(`${chalk.red(`✗`)} Account not found`); + return 1; + } + + await client.openIdAccount.delete({ + where: { + id: account.id, + }, + }); + + console.log( + `${chalk.green(`✓`)} Disconnected OpenID account from user ${chalk.blue(account.User?.username)}` + ); + + return 0; + }, + "Disconnects an OpenID Connect account from a local account", + "bun cli user oidc disconnect 123456789" + ), + new CliCommand<{ + id: string; + help: boolean; + noconfirm: boolean; + }>( + ["note", "delete"], + [ + { + name: "id", + type: CliParameterType.STRING, + description: "ID of the note", + needsValue: true, + positioned: true, + }, + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + }, + { + name: "noconfirm", + shortName: "y", + type: CliParameterType.EMPTY, + description: "Skip confirmation", + needsValue: false, + positioned: false, + }, + ], + async (instance: CliCommand, args) => { + const { id, help } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (!id) { + console.log(`${chalk.red(`✗`)} Missing ID`); + return 1; + } + + const note = await client.status.findFirst({ + where: { + id: id, + }, + }); + + if (!note) { + console.log(`${chalk.red(`✗`)} Note not found`); + return 1; + } + + if (!args.noconfirm) { + process.stdout.write( + `Are you sure you want to delete note ${chalk.blue( + note.id + )}?\n${chalk.red(chalk.bold("This is a destructive action and cannot be undone!"))} [y/N] ` + ); + + for await (const line of console) { + if (line.trim().toLowerCase() === "y") { + break; + } else { + console.log(`${chalk.red(`✗`)} Deletion cancelled`); + return 0; + } + } + } + + await client.status.delete({ + where: { + id: note.id, + }, + }); + + console.log( + `${chalk.green(`✓`)} Deleted note ${chalk.blue(note.id)}` + ); + + return 0; + }, + "Deletes a note", + "bun cli note delete 018c1838-6e0b-73c4-a157-a91ea4e25d1d" + ), + new CliCommand<{ + query: string; + fields: string[]; + local: boolean; + remote: boolean; + format: string; + help: boolean; + "case-sensitive": boolean; + limit: number; + redact: boolean; + }>( + ["note", "search"], + [ + { + name: "query", + type: CliParameterType.STRING, + description: "Query to search for", + needsValue: true, + positioned: true, + }, + { + name: "fields", + type: CliParameterType.ARRAY, + description: "Fields to search in", + needsValue: true, + positioned: false, + }, + { + name: "local", + type: CliParameterType.BOOLEAN, + description: "Only search in local statuses", + needsValue: false, + positioned: false, + optional: true, + }, + { + name: "remote", + type: CliParameterType.BOOLEAN, + description: "Only search in remote statuses", + needsValue: false, + positioned: false, + optional: true, + }, + { + name: "format", + type: CliParameterType.STRING, + description: "Output format (can be json or csv)", + needsValue: true, + positioned: false, + optional: true, + }, + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + }, + { + name: "case-sensitive", + shortName: "c", + type: CliParameterType.EMPTY, + description: "Case-sensitive search", + needsValue: false, + positioned: false, + optional: true, + }, + { + name: "limit", + type: CliParameterType.NUMBER, + description: "Limit the number of notes to list (default 20)", + needsValue: true, + positioned: false, + optional: true, + }, + { + name: "redact", + type: CliParameterType.BOOLEAN, + description: + "Redact sensitive information (such as password hashes, emails or keys)", + needsValue: false, + positioned: false, + optional: true, + }, + ], + async (instance: CliCommand, args) => { + const { + query, + local, + remote, + format, + help, + limit = 20, + fields = [], + "case-sensitive": caseSensitive = false, + redact, + } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (!query) { + console.log(`${chalk.red(`✗`)} Missing query parameter`); + return 1; + } + + if (fields.length === 0) { + console.log(`${chalk.red(`✗`)} Missing fields parameter`); + return 1; + } + + const queries: Prisma.StatusWhereInput[] = []; + + for (const field of fields) { + queries.push({ + [field]: { + contains: query, + mode: caseSensitive ? "default" : "insensitive", + }, + }); + } + + let instanceIdQuery; + + if (local && remote) { + instanceIdQuery = undefined; + } else if (local) { + instanceIdQuery = null; + } else if (remote) { + instanceIdQuery = { + not: null, + }; + } else { + instanceIdQuery = undefined; + } + + const notes = await client.status.findMany({ + where: { + OR: queries, + instanceId: instanceIdQuery, + }, + include: { + author: true, + instance: true, + }, + take: limit, + }); + + if (redact) { + for (const note of notes) { + note.author.email = "[REDACTED]"; + note.author.password = "[REDACTED]"; + note.author.publicKey = "[REDACTED]"; + note.author.privateKey = "[REDACTED]"; + } + } + + if (format === "json") { + console.log(JSON.stringify(notes, null, 4)); + return 0; + } else if (format === "csv") { + const parser = new Parser({}); + console.log(parser.parse(notes)); + return 0; + } + + console.log( + `${chalk.green(`✓`)} Found ${chalk.blue(notes.length)} notes (limit ${limit})` + ); + + const table = new Table({ + head: [ + chalk.white(chalk.bold("ID")), + chalk.white(chalk.bold("Content")), + chalk.white(chalk.bold("Author")), + chalk.white(chalk.bold("Instance")), + chalk.white(chalk.bold("Created At")), + ], + }); + + for (const note of notes) { + table.push([ + chalk.yellow(note.id), + chalk.green(note.content), + chalk.blue(note.author.username), + chalk.red( + note.instanceId ? note.instance?.base_url : "Yes" + ), + chalk.blue(note.createdAt.toISOString()), + ]); + } + + console.log(table.toString()); + + return 0; + }, + "Searches for a status", + "bun cli note search hello --fields content --local" + ), + new CliCommand<{ + help: boolean; + type: string[]; + }>( + ["index", "rebuild"], + [ + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + }, + { + name: "type", + type: CliParameterType.ARRAY, + description: + "Type(s) of index(es) to rebuild (can be accounts or statuses)", + needsValue: true, + positioned: false, + optional: true, + }, + ], + async (instance: CliCommand, args) => { + const { help, type = [] } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + // Check if Meilisearch is enabled + if (!config.meilisearch.enabled) { + console.log(`${chalk.red(`✗`)} Meilisearch is not enabled`); + return 1; + } + + // Check type validity + for (const _type of type) { + if ( + !Object.values(MeiliIndexType).includes( + _type as MeiliIndexType + ) + ) { + console.log( + `${chalk.red(`✗`)} Invalid index type ${chalk.blue(_type)}` + ); + return 1; + } + } + + if (type.length === 0) { + // Rebuild all indexes + await rebuildSearchIndexes(Object.values(MeiliIndexType)); + } else { + await rebuildSearchIndexes(type as MeiliIndexType[]); + } + + console.log(`${chalk.green(`✓`)} Rebuilt search indexes`); + + return 0; + }, + "Rebuilds the Meilisearch indexes", + "bun cli index rebuild" + ), + new CliCommand<{ + help: boolean; + shortcode: string; + url: string; + "keep-url": boolean; + }>( + ["emoji", "add"], + [ + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + optional: true, + }, + { + name: "shortcode", + type: CliParameterType.STRING, + description: "Shortcode of the new emoji", + needsValue: true, + positioned: true, + }, + { + name: "url", + type: CliParameterType.STRING, + description: "URL of the new emoji", + needsValue: true, + positioned: true, + }, + { + name: "keep-url", + type: CliParameterType.BOOLEAN, + description: + "Keep the URL of the emoji instead of uploading the file to object storage", + needsValue: false, + positioned: false, + }, + ], + async (instance: CliCommand, args) => { + const { help, shortcode, url } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (!shortcode) { + console.log(`${chalk.red(`✗`)} Missing shortcode`); + return 1; + } + if (!url) { + console.log(`${chalk.red(`✗`)} Missing URL`); + return 1; + } + + // Check if shortcode is valid + if (!shortcode.match(/^[a-zA-Z0-9-_]+$/)) { + console.log( + `${chalk.red(`✗`)} Invalid shortcode (must be alphanumeric with dashes and underscores allowed)` + ); + return 1; + } + + // Check if URL is valid + if (!URL.canParse(url)) { + console.log( + `${chalk.red(`✗`)} Invalid URL (must be a valid full URL, including protocol)` + ); + return 1; + } + + // Check if emoji already exists + const existingEmoji = await client.emoji.findFirst({ + where: { + shortcode: shortcode, + instanceId: null, + }, + }); + + if (existingEmoji) { + console.log( + `${chalk.red(`✗`)} Emoji with shortcode ${chalk.blue( + shortcode + )} already exists` + ); + return 1; + } + + let newUrl = url; + + if (!args["keep-url"]) { + // Upload the emoji to object storage + const mediaBackend = await MediaBackend.fromBackendType( + config.media.backend, + config + ); + + console.log( + `${chalk.blue(`⏳`)} Downloading emoji from ${chalk.underline(chalk.blue(url))}` + ); + + const downloadedFile = await fetch(url).then( + async r => + new File( + [await r.blob()], + url.split("/").pop() ?? + `${crypto.randomUUID()}-emoji.png` + ) + ); + + const metadata = await mediaBackend + .addFile(downloadedFile) + .catch(() => null); + + if (!metadata) { + console.log( + `${chalk.red(`✗`)} Failed to upload emoji to object storage (is your URL accessible?)` + ); + return 1; + } + + newUrl = getUrl(metadata.uploadedFile.name, config); + + console.log( + `${chalk.green(`✓`)} Uploaded emoji to object storage` + ); + } + + // Add the emoji + const content_type = `image/${url + .split(".") + .pop() + ?.replace("jpg", "jpeg")}}`; + + const emoji = await client.emoji.create({ + data: { + shortcode: shortcode, + url: newUrl, + visible_in_picker: true, + content_type: content_type, + instanceId: null, + }, + }); + + console.log( + `${chalk.green(`✓`)} Created emoji ${chalk.blue( + emoji.shortcode + )}` + ); + + return 0; + }, + "Adds a custom emoji", + "bun cli emoji add bun https://bun.com/bun.png" + ), + new CliCommand<{ + help: boolean; + shortcode: string; + noconfirm: boolean; + }>( + ["emoji", "delete"], + [ + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + optional: true, + }, + { + name: "shortcode", + type: CliParameterType.STRING, + description: + "Shortcode of the emoji to delete (can add up to two wildcards *)", + needsValue: true, + positioned: true, + }, + { + name: "noconfirm", + type: CliParameterType.BOOLEAN, + description: "Skip confirmation", + needsValue: false, + positioned: false, + optional: true, + }, + ], + async (instance: CliCommand, args) => { + const { help, shortcode, noconfirm } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (!shortcode) { + console.log(`${chalk.red(`✗`)} Missing shortcode`); + return 1; + } + + // Check if shortcode is valid + if (!shortcode.match(/^[a-zA-Z0-9-_*]+$/)) { + console.log( + `${chalk.red(`✗`)} Invalid shortcode (must be alphanumeric with dashes and underscores allowed + optional wildcards)` + ); + return 1; + } + + // Validate up to one wildcard + if (shortcode.split("*").length > 3) { + console.log( + `${chalk.red(`✗`)} Invalid shortcode (can only have up to two wildcards)` + ); + return 1; + } + + const hasWildcard = shortcode.includes("*"); + const hasTwoWildcards = shortcode.split("*").length === 3; + + const emojis = await client.emoji.findMany({ + where: { + shortcode: { + startsWith: hasWildcard + ? shortcode.split("*")[0] + : undefined, + endsWith: hasWildcard + ? shortcode.split("*").at(-1) + : undefined, + contains: hasTwoWildcards + ? shortcode.split("*")[1] + : undefined, + equals: hasWildcard ? undefined : shortcode, + }, + instanceId: null, + }, + }); + + if (emojis.length === 0) { + console.log( + `${chalk.red(`✗`)} No emoji with shortcode ${chalk.blue( + shortcode + )} found` + ); + return 1; + } + + // List emojis and ask for confirmation + for (const emoji of emojis) { + console.log( + `${chalk.blue(emoji.shortcode)}: ${chalk.underline( + emoji.url + )}` + ); + } + + if (!noconfirm) { + process.stdout.write( + `Are you sure you want to delete these emojis?\n${chalk.red(chalk.bold("This is a destructive action and cannot be undone!"))} [y/N] ` + ); + + for await (const line of console) { + if (line.trim().toLowerCase() === "y") { + break; + } else { + console.log(`${chalk.red(`✗`)} Deletion cancelled`); + return 0; + } + } + } + + await client.emoji.deleteMany({ + where: { + id: { + in: emojis.map(e => e.id), + }, + }, + }); + + console.log( + `${chalk.green(`✓`)} Deleted emojis matching shortcode ${chalk.blue( + shortcode + )}` + ); + + return 0; + }, + "Deletes custom emojis", + "bun cli emoji delete bun" + ), + new CliCommand<{ + help: boolean; + format: string; + limit: number; + }>( + ["emoji", "list"], + [ + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + optional: true, + }, + { + name: "format", + type: CliParameterType.STRING, + description: "Output format (can be json or csv)", + needsValue: true, + positioned: false, + optional: true, + }, + { + name: "limit", + type: CliParameterType.NUMBER, + description: "Limit the number of emojis to list (default 20)", + needsValue: true, + positioned: false, + optional: true, + }, + ], + async (instance: CliCommand, args) => { + const { help, format, limit = 20 } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + const emojis = await client.emoji.findMany({ + where: { + instanceId: null, + }, + take: limit, + }); + + if (format === "json") { + console.log(JSON.stringify(emojis, null, 4)); + return 0; + } else if (format === "csv") { + const parser = new Parser({}); + console.log(parser.parse(emojis)); + return 0; + } + + console.log( + `${chalk.green(`✓`)} Found ${chalk.blue(emojis.length)} emojis (limit ${limit})` + ); + + const table = new Table({ + head: [ + chalk.white(chalk.bold("Shortcode")), + chalk.white(chalk.bold("URL")), + ], + }); + + for (const emoji of emojis) { + table.push([ + chalk.blue(emoji.shortcode), + chalk.underline(emoji.url), + ]); + } + + console.log(table.toString()); + + return 0; + }, + "Lists all custom emojis", + "bun cli emoji list" + ), + new CliCommand<{ + help: boolean; + url: string; + noconfirm: boolean; + }>( + ["emoji", "import"], + [ + { + name: "help", + shortName: "h", + type: CliParameterType.EMPTY, + description: "Show help message", + needsValue: false, + positioned: false, + optional: true, + }, + { + name: "url", + type: CliParameterType.STRING, + description: "URL of the emoji pack manifest", + needsValue: true, + positioned: true, + }, + { + name: "noconfirm", + type: CliParameterType.BOOLEAN, + description: "Skip confirmation", + needsValue: false, + positioned: false, + optional: true, + }, + ], + async (instance: CliCommand, args) => { + const { help, url, noconfirm } = args; + + if (help) { + instance.displayHelp(); + return 0; + } + + if (!url) { + console.log(`${chalk.red(`✗`)} Missing URL`); + return 1; + } + + // Check if URL is valid + if (!URL.canParse(url)) { + console.log( + `${chalk.red(`✗`)} Invalid URL (must be a valid full URL, including protocol)` + ); + return 1; + } + + // Fetch the emoji pack manifest + const manifest = await fetch(url) + .then( + r => + r.json() as Promise< + Record< + string, + { + files: string; + homepage: string; + src: string; + src_sha256?: string; + } + > + > + ) + .catch(() => null); + + if (!manifest) { + console.log( + `${chalk.red(`✗`)} Failed to fetch emoji pack manifest from ${chalk.underline( + url + )}` + ); + return 1; + } + + const homepage = Object.values(manifest)[0].homepage; + // If URL is not a valid URL, assume it's a relative path to homepage + const srcUrl = URL.canParse(Object.values(manifest)[0].src) + ? Object.values(manifest)[0].src + : new URL(Object.values(manifest)[0].src, homepage).toString(); + const filesUrl = URL.canParse(Object.values(manifest)[0].files) + ? Object.values(manifest)[0].files + : new URL( + Object.values(manifest)[0].files, + homepage + ).toString(); + + console.log( + `${chalk.blue(`⏳`)} Fetching emoji pack from ${chalk.underline( + srcUrl + )}` + ); + + // Fetch actual pack (should be a zip file) + const pack = await fetch(srcUrl) + .then( + async r => + new File( + [await r.blob()], + srcUrl.split("/").pop() ?? "pack.zip" + ) + ) + .catch(() => null); + + // Check if pack is valid + if (!pack) { + console.log( + `${chalk.red(`✗`)} Failed to fetch emoji pack from ${chalk.underline( + srcUrl + )}` + ); + return 1; + } + + // Validate sha256 if available + if (Object.values(manifest)[0].src_sha256) { + const sha256 = new Bun.SHA256() + .update(await pack.arrayBuffer()) + .digest("hex"); + if (sha256 !== Object.values(manifest)[0].src_sha256) { + console.log( + `${chalk.red(`✗`)} SHA256 of pack (${chalk.blue( + sha256 + )}) does not match manifest ${chalk.blue( + Object.values(manifest)[0].src_sha256 + )}` + ); + return 1; + } else { + console.log( + `${chalk.green(`✓`)} SHA256 of pack matches manifest` + ); + } + } else { + console.log( + `${chalk.yellow(`⚠`)} No SHA256 in manifest, skipping validation` + ); + } + + console.log( + `${chalk.green(`✓`)} Fetched emoji pack from ${chalk.underline(srcUrl)}, unzipping to tempdir` + ); + + // Unzip the pack to temp dir + const tempDir = await mkdtemp(join(tmpdir(), "bun-emoji-import-")); + + console.log(join(tempDir, pack.name)); + + // Put the pack as a file + await Bun.write(join(tempDir, pack.name), pack); + + await extract(join(tempDir, pack.name), { + dir: tempDir, + }); + + console.log( + `${chalk.green(`✓`)} Unzipped emoji pack to ${chalk.blue(tempDir)}` + ); + + console.log( + `${chalk.blue(`⏳`)} Fetching emoji pack file metadata from ${chalk.underline( + filesUrl + )}` + ); + + // Fetch files URL + const packFiles = await fetch(filesUrl) + .then(r => r.json() as Promise>) + .catch(() => null); + + if (!packFiles) { + console.log( + `${chalk.red(`✗`)} Failed to fetch emoji pack file metadata from ${chalk.underline( + filesUrl + )}` + ); + return 1; + } + + console.log( + `${chalk.green(`✓`)} Fetched emoji pack file metadata from ${chalk.underline( + filesUrl + )}` + ); + + if (Object.keys(packFiles).length === 0) { + console.log(`${chalk.red(`✗`)} Empty emoji pack`); + return 1; + } + + if (!noconfirm) { + process.stdout.write( + `Are you sure you want to import ${chalk.blue( + Object.keys(packFiles).length + )} emojis from ${chalk.underline(chalk.blue(url))}? [y/N] ` + ); + + for await (const line of console) { + if (line.trim().toLowerCase() === "y") { + break; + } else { + console.log(`${chalk.red(`✗`)} Import cancelled`); + return 0; + } + } + } + + const successfullyImported: string[] = []; + + // Add emojis + for (const [shortcode, url] of Object.entries(packFiles)) { + // If emoji URL is not a valid URL, assume it's a relative path to homepage + const fileUrl = Bun.pathToFileURL( + join(tempDir, url) + ).toString(); + + // Check if emoji already exists + const existingEmoji = await client.emoji.findFirst({ + where: { + shortcode: shortcode, + instanceId: null, + }, + }); + + if (existingEmoji) { + console.log( + `${chalk.red(`✗`)} Emoji with shortcode ${chalk.blue( + shortcode + )} already exists` + ); + continue; + } + + // Add the emoji by calling the add command + const returnCode = await cliBuilder.processArgs([ + "emoji", + "add", + shortcode, + fileUrl, + "--noconfirm", + ]); + + if (returnCode === 0) successfullyImported.push(shortcode); + } + + console.log( + `${chalk.green(`✓`)} Imported ${successfullyImported.length} emojis from ${chalk.underline( + url + )}` + ); + + // List imported + if (successfullyImported.length > 0) { + console.log( + `${chalk.green(`✓`)} Successfully imported ${successfullyImported.length} emojis: ${successfullyImported.join( + ", " + )}` + ); + } + + // List unimported + if (successfullyImported.length < Object.keys(packFiles).length) { + const unimported = Object.keys(packFiles).filter( + key => !successfullyImported.includes(key) + ); + console.log( + `${chalk.red(`✗`)} Failed to import ${unimported.length} emojis: ${unimported.join( + ", " + )}` + ); + } + + return 0; + }, + "Imports a Pleroma emoji pack", + "bun cli emoji import https://site.com/neofox/manifest.json" + ), ]); -cliBuilder.processArgs(args); +// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression +const exitCode = await 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) => { - // Remove formatting codes - // eslint-disable-next-line no-control-regex - const textLength = text.replace(/\u001b\[\d+m/g, "").length; - const dots = ".".repeat(length - textLength); - return `${text}${chalk.gray(dots)}`; -}; - -const alignDotsSmall = (text: string, length = 16) => alignDots(text, length); - -const help = ` -${chalk.bold(`Usage: bun cli ${chalk.blue("[...flags]")} [...args]`)} - -${chalk.bold("Commands:")} - ${alignDots(chalk.blue("help"), 24)} Show this help message - ${alignDots(chalk.blue("user"), 24)} Manage users - ${alignDots(chalk.blue("create"))} Create a new user - ${alignDotsSmall(chalk.green("username"))} Username of the user - ${alignDotsSmall(chalk.green("password"))} Password of the user - ${alignDotsSmall(chalk.green("email"))} Email of the user - ${alignDotsSmall( - chalk.yellow("--admin") - )} Make the user an admin (optional) - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli user create admin password123 admin@gmail.com --admin` - )} - ${alignDots(chalk.blue("delete"))} Delete a user - ${alignDotsSmall(chalk.green("username"))} Username of the user - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli user delete admin` - )} - ${alignDots(chalk.blue("list"))} List all users - ${alignDotsSmall( - chalk.yellow("--admins") - )} List only admins (optional) - ${chalk.bold("Example:")} ${chalk.bgGray(`bun cli user list`)} - ${alignDots(chalk.blue("search"))} Search for a user - ${alignDotsSmall(chalk.green("query"))} Query to search for - ${alignDotsSmall( - chalk.yellow("--displayname") - )} Search by display name (optional) - ${alignDotsSmall(chalk.yellow("--bio"))} Search in bio (optional) - ${alignDotsSmall( - chalk.yellow("--local") - )} Search in local users (optional) - ${alignDotsSmall( - chalk.yellow("--remote") - )} Search in remote users (optional) - ${alignDotsSmall( - chalk.yellow("--email") - )} Search in emails (optional) - ${alignDotsSmall(chalk.yellow("--json"))} Output as JSON (optional) - ${alignDotsSmall(chalk.yellow("--csv"))} Output as CSV (optional) - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli user search admin` - )} - ${alignDots( - chalk.blue("connect-openid") - )} Connect an OpenID account to a local account - ${alignDotsSmall( - chalk.green("username") - )} Username of the local account - ${alignDotsSmall(chalk.green("issuerId"))} ID of the OpenID issuer - ${alignDotsSmall( - chalk.green("serverId") - )} ID of the user on the OpenID server - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli user connect-openid admin google 123456789` - )} - ${alignDots(chalk.blue("note"), 24)} Manage notes - ${alignDots(chalk.blue("delete"))} Delete a note - ${alignDotsSmall(chalk.green("id"))} ID of the note - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli note delete 018c1838-6e0b-73c4-a157-a91ea4e25d1d` - )} - ${alignDots(chalk.blue("search"))} Search for a status - ${alignDotsSmall(chalk.green("query"))} Query to search for - ${alignDotsSmall( - chalk.yellow("--local") - )} Search in local statuses (optional) - ${alignDotsSmall( - chalk.yellow("--remote") - )} Search in remote statuses (optional) - ${alignDotsSmall(chalk.yellow("--json"))} Output as JSON (optional) - ${alignDotsSmall(chalk.yellow("--csv"))} Output as CSV (optional) - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli note search hello` - )} - ${alignDots(chalk.blue("index"), 24)} Manage user and status indexes - ${alignDots(chalk.blue("rebuild"))} Rebuild the index - ${alignDotsSmall( - chalk.green("batch-size") - )} The number of items to index at once (optional, default 100) - ${alignDotsSmall( - chalk.yellow("--statuses") - )} Only rebuild the statuses index (optional) - ${alignDotsSmall( - chalk.yellow("--users") - )} Only rebuild the users index (optional) - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli index rebuild --users 200` - )} - ${alignDots(chalk.blue("emoji"), 24)} Manage custom emojis - ${alignDots(chalk.blue("add"))} Add a custom emoji - ${alignDotsSmall(chalk.green("name"))} Name of the emoji - ${alignDotsSmall(chalk.green("url"))} URL of the emoji - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli emoji add bun https://bun.com/bun.png` - )} - ${alignDots(chalk.blue("delete"))} Delete a custom emoji - ${alignDotsSmall(chalk.green("name"))} Name of the emoji - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli emoji delete bun` - )} - ${alignDots(chalk.blue("list"))} List all custom emojis - ${chalk.bold("Example:")} ${chalk.bgGray(`bun cli emoji list`)} - ${alignDots(chalk.blue("search"))} Search for a custom emoji - ${alignDotsSmall(chalk.green("query"))} Query to search for - ${alignDotsSmall( - chalk.yellow("--local") - )} Search in local emojis (optional, default) - ${alignDotsSmall( - chalk.yellow("--remote") - )} Search in remote emojis (optional) - ${alignDotsSmall(chalk.yellow("--json"))} Output as JSON (optional) - ${alignDotsSmall(chalk.yellow("--csv"))} Output as CSV (optional) - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli emoji search bun` - )} - ${alignDots(chalk.blue("import"))} Import a Pleroma emoji pack - ${alignDotsSmall(chalk.green("url"))} URL of the emoji pack - ${chalk.bold("Example:")} ${chalk.bgGray( - `bun cli emoji import https://site.com/neofox/manifest.json` - )} -`; - -if (args.length < 3) { - console.log(help); - process.exit(0); -} - -const command = args[2]; - -const config = getConfig(); - -switch (command) { - case "help": - console.log(help); - break; - case "user": - switch (args[3]) { - case "create": { - // Check if --admin flag is provided - const argsWithFlags = args.filter(arg => arg.startsWith("--")); - const argsWithoutFlags = args.filter( - arg => !arg.startsWith("--") - ); - - const username = argsWithoutFlags[4]; - const password = argsWithoutFlags[5]; - const email = argsWithoutFlags[6]; - - const admin = argsWithFlags.includes("--admin"); - - // Check if username, password and email are provided - if (!username || !password || !email) { - console.log( - `${chalk.red(`✗`)} Missing username, password or email` - ); - process.exit(1); - } - - // Check if user already exists - const user = await client.user.findFirst({ - where: { - OR: [{ username }, { email }], - }, - }); - - if (user) { - console.log(`${chalk.red(`✗`)} User already exists`); - process.exit(1); - } - - // 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)") : ""}` - ); - break; - } - case "delete": { - const username = args[4]; - - if (!username) { - console.log(`${chalk.red(`✗`)} Missing username`); - process.exit(1); - } - - const user = await client.user.findFirst({ - where: { - username: username, - }, - }); - - if (!user) { - console.log(`${chalk.red(`✗`)} User not found`); - process.exit(1); - } - - await client.user.delete({ - where: { - id: user.id, - }, - }); - - console.log( - `${chalk.green(`✓`)} Deleted user ${chalk.blue( - user.username - )}` - ); - - break; - } - case "list": { - const admins = args.includes("--admins"); - - const users = await client.user.findMany({ - where: { - isAdmin: admins || undefined, - }, - take: 200, - }); - - console.log( - `${chalk.green(`✓`)} Found ${chalk.blue( - users.length - )} users` - ); - - for (const user of users) { - console.log( - `\t${chalk.blue(user.username)} ${chalk.gray( - user.email - )} ${chalk.green(user.isAdmin ? "Admin" : "User")}` - ); - } - break; - } - case "search": { - const argsWithoutFlags = args.filter( - arg => !arg.startsWith("--") - ); - const query = argsWithoutFlags[4]; - - if (!query) { - console.log(`${chalk.red(`✗`)} Missing query`); - process.exit(1); - } - - const displayname = args.includes("--displayname"); - const bio = args.includes("--bio"); - const local = args.includes("--local"); - const remote = args.includes("--remote"); - const email = args.includes("--email"); - const json = args.includes("--json"); - const csv = args.includes("--csv"); - - const queries: Prisma.UserWhereInput[] = []; - - if (displayname) { - queries.push({ - displayName: { - contains: query, - mode: "insensitive", - }, - }); - } - - if (bio) { - queries.push({ - note: { - contains: query, - mode: "insensitive", - }, - }); - } - - if (local) { - queries.push({ - instanceId: null, - }); - } - - if (remote) { - queries.push({ - instanceId: { - not: null, - }, - }); - } - - if (email) { - queries.push({ - email: { - contains: query, - mode: "insensitive", - }, - }); - } - - const users = await client.user.findMany({ - where: { - AND: queries, - }, - include: { - instance: true, - }, - take: 40, - }); - - if (json || csv) { - if (json) { - console.log(JSON.stringify(users, null, 4)); - } - if (csv) { - // Convert the outputted JSON to CSV - - // Remove all object children from each object - const items = users.map(user => { - const item = { - ...user, - instance: undefined, - endpoints: undefined, - source: undefined, - }; - return item; - }); - const replacer = (key: string, value: any): any => - value === null ? "" : value; // Null values are returned as empty strings - const header = Object.keys(items[0]); - const csv = [ - header.join(","), // header row first - ...items.map(row => - header - .map(fieldName => - // @ts-expect-error This is fine - JSON.stringify(row[fieldName], replacer) - ) - .join(",") - ), - ].join("\r\n"); - - console.log(csv); - } - } else { - console.log( - `${chalk.green(`✓`)} Found ${chalk.blue( - users.length - )} users` - ); - - const table = new Table({ - head: [ - chalk.white(chalk.bold("Username")), - chalk.white(chalk.bold("Email")), - chalk.white(chalk.bold("Display Name")), - chalk.white(chalk.bold("Admin?")), - chalk.white(chalk.bold("Instance URL")), - ], - }); - - for (const user of users) { - table.push([ - chalk.yellow(`@${user.username}`), - chalk.green(user.email), - chalk.blue(user.displayName), - chalk.red(user.isAdmin ? "Yes" : "No"), - chalk.blue( - user.instanceId - ? user.instance?.base_url - : "Local" - ), - ]); - } - - console.log(table.toString()); - } - - break; - } - case "connect-openid": { - const username = args[4]; - const issuerId = args[5]; - const serverId = args[6]; - - if (!username || !issuerId || !serverId) { - console.log( - `${chalk.red(`✗`)} Missing username, issuer or ID` - ); - process.exit(1); - } - - const user = await client.user.findFirst({ - where: { - username: username, - }, - }); - - if (!user) { - console.log(`${chalk.red(`✗`)} User not found`); - process.exit(1); - } - - const issuer = config.oidc.providers.find( - p => p.id === issuerId - ); - - if (!issuer) { - console.log(`${chalk.red(`✗`)} Issuer not found`); - process.exit(1); - } - - await client.user.update({ - where: { - id: user.id, - }, - data: { - linkedOpenIdAccounts: { - create: { - issuerId: issuerId, - serverId: serverId, - }, - }, - }, - }); - - console.log( - `${chalk.green( - `✓` - )} Connected OpenID account to user ${chalk.blue( - user.username - )}` - ); - - break; - } - default: - console.log(`Unknown command ${chalk.blue(command)}`); - break; - } - break; - case "note": { - switch (args[3]) { - case "delete": { - const id = args[4]; - - if (!id) { - console.log(`${chalk.red(`✗`)} Missing ID`); - process.exit(1); - } - - const note = await client.status.findFirst({ - where: { - id: id, - }, - }); - - if (!note) { - console.log(`${chalk.red(`✗`)} Note not found`); - process.exit(1); - } - - await client.status.delete({ - where: { - id: note.id, - }, - }); - - console.log( - `${chalk.green(`✓`)} Deleted note ${chalk.blue(note.id)}` - ); - - break; - } - case "search": { - const argsWithoutFlags = args.filter( - arg => !arg.startsWith("--") - ); - const query = argsWithoutFlags[4]; - - if (!query) { - console.log(`${chalk.red(`✗`)} Missing query`); - process.exit(1); - } - - const local = args.includes("--local"); - const remote = args.includes("--remote"); - const json = args.includes("--json"); - const csv = args.includes("--csv"); - - const queries: Prisma.StatusWhereInput[] = []; - - if (local) { - queries.push({ - instanceId: null, - }); - } - - if (remote) { - queries.push({ - instanceId: { - not: null, - }, - }); - } - - const statuses = await client.status.findMany({ - where: { - AND: queries, - content: { - contains: query, - mode: "insensitive", - }, - }, - take: 40, - include: { - author: true, - instance: true, - }, - }); - - if (json || csv) { - if (json) { - console.log(JSON.stringify(statuses, null, 4)); - } - if (csv) { - // Convert the outputted JSON to CSV - - // Remove all object children from each object - const items = statuses.map(status => { - const item = { - ...status, - author: undefined, - instance: undefined, - }; - return item; - }); - const replacer = (key: string, value: any): any => - value === null ? "" : value; // Null values are returned as empty strings - const header = Object.keys(items[0]); - const csv = [ - header.join(","), // header row first - ...items.map(row => - header - .map(fieldName => - // @ts-expect-error This is fine - JSON.stringify(row[fieldName], replacer) - ) - .join(",") - ), - ].join("\r\n"); - - console.log(csv); - } - } else { - console.log( - `${chalk.green(`✓`)} Found ${chalk.blue( - statuses.length - )} statuses` - ); - - const table = new Table({ - head: [ - chalk.white(chalk.bold("Username")), - chalk.white(chalk.bold("Instance URL")), - chalk.white(chalk.bold("Content")), - ], - }); - - for (const status of statuses) { - table.push([ - chalk.yellow(`@${status.author.username}`), - chalk.blue( - status.instanceId - ? status.instance?.base_url - : "Local" - ), - chalk.green(status.content.slice(0, 50)), - ]); - } - - console.log(table.toString()); - } - - break; - } - default: - console.log(`Unknown command ${chalk.blue(command)}`); - break; - } - break; - } - case "index": { - if (!config.meilisearch.enabled) { - console.log( - `${chalk.red(`✗`)} Meilisearch is not enabled in the config` - ); - process.exit(1); - } - switch (args[3]) { - case "rebuild": { - const statuses = args.includes("--statuses"); - const users = args.includes("--users"); - - const argsWithoutFlags = args.filter( - arg => !arg.startsWith("--") - ); - - const batchSize = Number(argsWithoutFlags[4]) || 100; - - const neither = !statuses && !users; - - if (statuses || neither) { - console.log( - `${chalk.yellow(`⚠`)} ${chalk.bold( - `Rebuilding Meilisearch index for statuses` - )}` - ); - - const timeBefore = performance.now(); - - await rebuildSearchIndexes( - [MeiliIndexType.Statuses], - batchSize - ); - - console.log( - `${chalk.green(`✓`)} ${chalk.bold( - `Meilisearch index for statuses rebuilt in ${chalk.bgGreen( - (performance.now() - timeBefore).toFixed(2) - )}ms` - )}` - ); - } - - if (users || neither) { - console.log( - `${chalk.yellow(`⚠`)} ${chalk.bold( - `Rebuilding Meilisearch index for users` - )}` - ); - - const timeBefore = performance.now(); - - await rebuildSearchIndexes( - [MeiliIndexType.Accounts], - batchSize - ); - - console.log( - `${chalk.green(`✓`)} ${chalk.bold( - `Meilisearch index for users rebuilt in ${chalk.bgGreen( - (performance.now() - timeBefore).toFixed(2) - )}ms` - )}` - ); - } - - break; - } - default: - console.log(`Unknown command ${chalk.blue(command)}`); - break; - } - break; - } - case "emoji": { - switch (args[3]) { - case "add": { - const name = args[4]; - const url = args[5]; - - if (!name || !url) { - console.log(`${chalk.red(`✗`)} Missing name or URL`); - process.exit(1); - } - - const content_type = `image/${url - .split(".") - .pop() - ?.replace("jpg", "jpeg")}}`; - - const emoji = await client.emoji.create({ - data: { - shortcode: name, - url: url, - visible_in_picker: true, - content_type: content_type, - }, - }); - - console.log( - `${chalk.green(`✓`)} Created emoji ${chalk.blue( - emoji.shortcode - )}` - ); - - break; - } - case "delete": { - const name = args[4]; - - if (!name) { - console.log(`${chalk.red(`✗`)} Missing name`); - process.exit(1); - } - - const emoji = await client.emoji.findFirst({ - where: { - shortcode: name, - }, - }); - - if (!emoji) { - console.log(`${chalk.red(`✗`)} Emoji not found`); - process.exit(1); - } - - await client.emoji.delete({ - where: { - id: emoji.id, - }, - }); - - console.log( - `${chalk.green(`✓`)} Deleted emoji ${chalk.blue( - emoji.shortcode - )}` - ); - - break; - } - case "list": { - const emojis = await client.emoji.findMany(); - - console.log( - `${chalk.green(`✓`)} Found ${chalk.blue( - emojis.length - )} emojis` - ); - - for (const emoji of emojis) { - console.log( - `\t${chalk.blue(emoji.shortcode)} ${chalk.gray( - emoji.url - )}` - ); - } - break; - } - case "search": { - const argsWithoutFlags = args.filter( - arg => !arg.startsWith("--") - ); - const query = argsWithoutFlags[4]; - - if (!query) { - console.log(`${chalk.red(`✗`)} Missing query`); - process.exit(1); - } - - const local = args.includes("--local"); - const remote = args.includes("--remote"); - const json = args.includes("--json"); - const csv = args.includes("--csv"); - - const queries: Prisma.EmojiWhereInput[] = []; - - if (local) { - queries.push({ - instanceId: null, - }); - } - - if (remote) { - queries.push({ - instanceId: { - not: null, - }, - }); - } - - const emojis = await client.emoji.findMany({ - where: { - AND: queries, - shortcode: { - contains: query, - mode: "insensitive", - }, - }, - take: 40, - include: { - instance: true, - }, - }); - - if (json || csv) { - if (json) { - console.log(JSON.stringify(emojis, null, 4)); - } - if (csv) { - // Convert the outputted JSON to CSV - - // Remove all object children from each object - const items = emojis.map(emoji => { - const item = { - ...emoji, - instance: undefined, - }; - return item; - }); - const replacer = (key: string, value: any): any => - value === null ? "" : value; // Null values are returned as empty strings - const header = Object.keys(items[0]); - const csv = [ - header.join(","), // header row first - ...items.map(row => - header - .map(fieldName => - // @ts-expect-error This is fine - JSON.stringify(row[fieldName], replacer) - ) - .join(",") - ), - ].join("\r\n"); - - console.log(csv); - } - } else { - console.log( - `${chalk.green(`✓`)} Found ${chalk.blue( - emojis.length - )} emojis` - ); - - const table = new Table({ - head: [ - chalk.white(chalk.bold("Shortcode")), - chalk.white(chalk.bold("Instance URL")), - chalk.white(chalk.bold("URL")), - ], - }); - - for (const emoji of emojis) { - table.push([ - chalk.yellow(`:${emoji.shortcode}:`), - chalk.blue( - emoji.instanceId - ? emoji.instance?.base_url - : "Local" - ), - chalk.gray(emoji.url), - ]); - } - - console.log(table.toString()); - } - - break; - } - case "import": { - const url = args[4]; - - if (!url) { - console.log(`${chalk.red(`✗`)} Missing URL`); - process.exit(1); - } - - const response = await fetch(url); - - if (!response.ok) { - console.log(`${chalk.red(`✗`)} Failed to fetch emoji pack`); - process.exit(1); - } - - const res = (await response.json()) as Record< - string, - { - description: string; - files: string; - homepage: string; - src: string; - src_sha256?: string; - license?: string; - } - >; - - const pack = Object.values(res)[0]; - - // Fetch emoji list from `files`, can be a relative URL - - if (!pack.files) { - console.log(`${chalk.red(`✗`)} Missing files`); - process.exit(1); - } - - let pack_url = pack.files; - - if (!pack.files.includes("http")) { - // Is relative URL to pack manifest URL - pack_url = - url.split("/").slice(0, -1).join("/") + - "/" + - pack.files; - } - - const zip = new File( - [await (await fetch(pack.src)).arrayBuffer()], - "emoji.zip", - { - type: "application/zip", - } - ); - - // Check if the SHA256 hash matches - const hasher = new Bun.SHA256(); - - hasher.update(await zip.arrayBuffer()); - - const hash = hasher.digest("hex"); - - if (pack.src_sha256 && pack.src_sha256 !== hash) { - console.log(`${chalk.red(`✗`)} SHA256 hash does not match`); - console.log( - `${chalk.red(`✗`)} Expected ${chalk.blue( - pack.src_sha256 - )}, got ${chalk.blue(hash)}` - ); - process.exit(1); - } - - // Store file in /tmp - const tempDirectory = `/tmp/lysand-${hash}`; - - if (!(await exists(tempDirectory))) { - await mkdir(tempDirectory); - } - - await Bun.write(`${tempDirectory}/emojis.zip`, zip); - - // Extract zip - await extract(`${tempDirectory}/emojis.zip`, { - dir: tempDirectory, - }); - - // In the format - // emoji_name: emoji_url - const pack_response = (await ( - await fetch(pack_url) - ).json()) as Record; - - let emojisCreated = 0; - - for (const [name, path] of Object.entries(pack_response)) { - // Check if emoji already exists - const existingEmoji = await client.emoji.findFirst({ - where: { - shortcode: name, - instanceId: null, - }, - }); - - if (existingEmoji) { - console.log( - `${chalk.red(`✗`)} Emoji ${chalk.blue( - name - )} already exists` - ); - continue; - } - - // Get emoji URL, as it can be relative - - const emoji = Bun.file(`${tempDirectory}/${path}`); - - const content_type = emoji.type; - - const hash = await uploadFile( - emoji as unknown as File, - config - ); - - if (!hash) { - console.log( - `${chalk.red(`✗`)} Failed to upload emoji ${name}` - ); - process.exit(1); - } - - const finalUrl = getUrl(hash, config); - - // Create emoji - await client.emoji.create({ - data: { - shortcode: name, - url: finalUrl, - visible_in_picker: true, - content_type: content_type, - }, - }); - - emojisCreated++; - - console.log( - `${chalk.green(`✓`)} Created emoji ${chalk.blue(name)}` - ); - } - - console.log( - `${chalk.green(`✓`)} Imported ${chalk.blue( - emojisCreated - )} emojis` - ); - - break; - } - default: - console.log(`Unknown command ${chalk.blue(command)}`); - break; - } - - break; - } - default: - console.log(`Unknown command ${chalk.blue(command)}`); - break; -} - -process.exit(0); - */ +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +process.exit(Number(exitCode ?? 0)); diff --git a/package.json b/package.json index 459001bb..01212141 100644 --- a/package.json +++ b/package.json @@ -84,11 +84,14 @@ "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", "chalk": "^5.3.0", + "cli-parser": "file:packages/cli-parser", "cli-table": "^0.3.11", + "config-manager": "file:packages/config-manager", "eventemitter3": "^5.0.1", "extract-zip": "^2.0.1", "html-to-text": "^9.0.5", @@ -100,7 +103,9 @@ "linkify-html": "^4.1.3", "linkify-string": "^4.1.3", "linkifyjs": "^4.1.3", + "log-manager": "file:packages/log-manager", "marked": "latest", + "media-manager": "file:packages/media-manager", "megalodon": "^9.1.1", "meilisearch": "latest", "merge-deep-ts": "^1.2.6", @@ -108,12 +113,8 @@ "oauth4webapi": "^2.4.0", "prisma": "^5.6.0", "prisma-redis-middleware": "^4.8.0", - "semver": "^7.5.4", - "sharp": "^0.33.0-rc.2", "request-parser": "file:packages/request-parser", - "config-manager": "file:packages/config-manager", - "cli-parser": "file:packages/cli-parser", - "log-manager": "file:packages/log-manager", - "media-manager": "file:packages/media-manager" + "semver": "^7.5.4", + "sharp": "^0.33.0-rc.2" } } \ No newline at end of file diff --git a/packages/cli-parser/index.ts b/packages/cli-parser/index.ts index eed8fb5c..126a90ae 100644 --- a/packages/cli-parser/index.ts +++ b/packages/cli-parser/index.ts @@ -117,7 +117,9 @@ export class CliBuilder { prev.categories.length > current.categories.length ? prev : current ); - const argsWithoutCategories = revelantArgs.slice(command.categories.length); + const argsWithoutCategories = revelantArgs.slice( + command.categories.length + ); return await command.run(argsWithoutCategories); } @@ -243,8 +245,6 @@ export class CliBuilder { }) ); - console.log(optimal_length) - for (const line of writeBuffer.split("\n")) { const [left, right] = line.split("|"); if (!right) { @@ -261,6 +261,7 @@ export class CliBuilder { type ExecuteFunction = ( instance: CliCommand, args: Partial + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type ) => Promise | Promise | number | void; /** @@ -364,7 +365,7 @@ ${unpositionedArgs currentParameter = null; } else { const positionedArgType = this.argTypes.find( - argType => argType.positioned + argType => argType.positioned && !parsedArgs[argType.name] ); if (positionedArgType) { parsedArgs[positionedArgType.name] = this.castArgValue( diff --git a/packages/cli-parser/tests/cli-builder.test.ts b/packages/cli-parser/tests/cli-builder.test.ts index b45d713c..96ffdb08 100644 --- a/packages/cli-parser/tests/cli-builder.test.ts +++ b/packages/cli-parser/tests/cli-builder.test.ts @@ -113,7 +113,7 @@ describe("CliCommand", () => { ).toEqual(["value1", "value2"]); }); - it("should run the execute function with the parsed parameters", () => { + it("should run the execute function with the parsed parameters", async () => { const mockExecute = jest.fn(); cliCommand = new CliCommand( ["category1", "category2"], @@ -142,7 +142,7 @@ describe("CliCommand", () => { mockExecute ); - cliCommand.run([ + await cliCommand.run([ "--arg1", "value1", "--arg2", @@ -159,7 +159,7 @@ describe("CliCommand", () => { }); }); - it("should work with a mix of positioned and non-positioned arguments", () => { + it("should work with a mix of positioned and non-positioned arguments", async () => { const mockExecute = jest.fn(); cliCommand = new CliCommand( ["category1", "category2"], @@ -194,7 +194,7 @@ describe("CliCommand", () => { mockExecute ); - cliCommand.run([ + await cliCommand.run([ "--arg1", "value1", "--arg2", @@ -324,7 +324,7 @@ describe("CliBuilder", () => { expect(cliBuilder.commands).not.toContain(mockCommand2); }); - it("should process args correctly", () => { + it("should process args correctly", async () => { const mockExecute = jest.fn(); const mockCommand = new CliCommand( ["category1", "sub1"], @@ -339,7 +339,7 @@ describe("CliBuilder", () => { mockExecute ); cliBuilder.registerCommand(mockCommand); - cliBuilder.processArgs([ + await cliBuilder.processArgs([ "./cli.ts", "category1", "sub1", diff --git a/packages/media-manager/index.ts b/packages/media-manager/index.ts index 501495b1..1dc24dba 100644 --- a/packages/media-manager/index.ts +++ b/packages/media-manager/index.ts @@ -32,6 +32,24 @@ export class MediaBackend { public backend: MediaBackendType ) {} + static async fromBackendType( + backend: MediaBackendType, + config: ConfigType + ): Promise { + switch (backend) { + case MediaBackendType.LOCAL: + return new (await import("./backends/local")).LocalMediaBackend( + config + ); + case MediaBackendType.S3: + return new (await import("./backends/s3")).S3MediaBackend( + config + ); + default: + throw new Error(`Unknown backend type: ${backend as any}`); + } + } + public getBackendType() { return this.backend; } diff --git a/packages/media-manager/tests/media-backends.test.ts b/packages/media-manager/tests/media-backends.test.ts index fc36068f..3470927e 100644 --- a/packages/media-manager/tests/media-backends.test.ts +++ b/packages/media-manager/tests/media-backends.test.ts @@ -30,6 +30,39 @@ describe("MediaBackend", () => { expect(mediaBackend.getBackendType()).toEqual(MediaBackendType.S3); }); + describe("fromBackendType", () => { + it("should return a LocalMediaBackend instance for LOCAL backend type", async () => { + const backend = await MediaBackend.fromBackendType( + MediaBackendType.LOCAL, + mockConfig + ); + expect(backend).toBeInstanceOf(LocalMediaBackend); + }); + + it("should return a S3MediaBackend instance for S3 backend type", async () => { + const backend = await MediaBackend.fromBackendType( + MediaBackendType.S3, + { + s3: { + endpoint: "localhost:4566", + region: "us-east-1", + bucket_name: "test-bucket", + access_key: "test-access", + public_url: "test", + secret_access_key: "test-secret", + }, + } as ConfigType + ); + expect(backend).toBeInstanceOf(S3MediaBackend); + }); + + it("should throw an error for unknown backend type", () => { + expect( + MediaBackend.fromBackendType("unknown" as any, mockConfig) + ).rejects.toThrow("Unknown backend type: unknown"); + }); + }); + it("should check if images should be converted", () => { expect(mediaBackend.shouldConvertImages(mockConfig)).toBe(true); mockConfig.media.conversion.convert_images = false;