mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat(cli): ✨ Add more emoji commands to CLI (add, delete, list, import)
This commit is contained in:
parent
e48f57a3d8
commit
5bdb8360ea
|
|
@ -1,9 +1,10 @@
|
||||||
import { Args, type Command, Flags, type Interfaces } from "@oclif/core";
|
import { Args, type Command, Flags, type Interfaces } from "@oclif/core";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { and, eq, like } from "drizzle-orm";
|
import { and, eq, getTableColumns, like } from "drizzle-orm";
|
||||||
import { Users } from "~drizzle/schema";
|
import { Emojis, Instances, Users } from "~drizzle/schema";
|
||||||
import { User } from "~packages/database-interface/user";
|
import { User } from "~packages/database-interface/user";
|
||||||
import { BaseCommand } from "./base";
|
import { BaseCommand } from "./base";
|
||||||
|
import { db } from "~drizzle/db";
|
||||||
|
|
||||||
export type FlagsType<T extends typeof Command> = Interfaces.InferredFlags<
|
export type FlagsType<T extends typeof Command> = Interfaces.InferredFlags<
|
||||||
(typeof BaseCommand)["baseFlags"] & T["flags"]
|
(typeof BaseCommand)["baseFlags"] & T["flags"]
|
||||||
|
|
@ -104,3 +105,91 @@ export abstract class UserFinderCommand<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export abstract class EmojiFinderCommand<
|
||||||
|
T extends typeof BaseCommand,
|
||||||
|
> extends BaseCommand<typeof EmojiFinderCommand> {
|
||||||
|
static baseFlags = {
|
||||||
|
pattern: Flags.boolean({
|
||||||
|
char: "p",
|
||||||
|
description:
|
||||||
|
"Process as a wildcard pattern (don't forget to escape)",
|
||||||
|
}),
|
||||||
|
type: Flags.string({
|
||||||
|
char: "t",
|
||||||
|
description: "Type of identifier",
|
||||||
|
options: ["shortcode", "instance"],
|
||||||
|
default: "shortcode",
|
||||||
|
}),
|
||||||
|
limit: Flags.integer({
|
||||||
|
char: "n",
|
||||||
|
description: "Limit the number of emojis",
|
||||||
|
default: 100,
|
||||||
|
}),
|
||||||
|
print: Flags.boolean({
|
||||||
|
allowNo: true,
|
||||||
|
default: true,
|
||||||
|
char: "P",
|
||||||
|
description: "Print emoji(s) found before processing",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static baseArgs = {
|
||||||
|
identifier: Args.string({
|
||||||
|
description: "Identifier of the emoji (defaults to shortcode)",
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
protected flags!: FlagsType<T>;
|
||||||
|
protected args!: ArgsType<T>;
|
||||||
|
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
await super.init();
|
||||||
|
const { args, flags } = await this.parse({
|
||||||
|
flags: this.ctor.flags,
|
||||||
|
baseFlags: (super.ctor as typeof BaseCommand).baseFlags,
|
||||||
|
args: this.ctor.args,
|
||||||
|
strict: this.ctor.strict,
|
||||||
|
});
|
||||||
|
this.flags = flags as FlagsType<T>;
|
||||||
|
this.args = args as ArgsType<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findEmojis() {
|
||||||
|
// Check if there are asterisks in the identifier but no pattern flag, warn the user if so
|
||||||
|
if (this.args.identifier.includes("*") && !this.flags.pattern) {
|
||||||
|
this.log(
|
||||||
|
chalk.bold(
|
||||||
|
`${chalk.yellow(
|
||||||
|
"⚠",
|
||||||
|
)} Your identifier has asterisks but the --pattern flag is not set. This will match a literal string. If you want to use wildcards, set the --pattern flag.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const operator = this.flags.pattern ? like : eq;
|
||||||
|
// Replace wildcards with an SQL LIKE pattern
|
||||||
|
const identifier = this.flags.pattern
|
||||||
|
? this.args.identifier.replace(/\*/g, "%")
|
||||||
|
: this.args.identifier;
|
||||||
|
|
||||||
|
return await db
|
||||||
|
.select({
|
||||||
|
...getTableColumns(Emojis),
|
||||||
|
instanceUrl: Instances.baseUrl,
|
||||||
|
})
|
||||||
|
.from(Emojis)
|
||||||
|
.leftJoin(Instances, eq(Emojis.instanceId, Instances.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
this.flags.type === "shortcode"
|
||||||
|
? operator(Emojis.shortcode, identifier)
|
||||||
|
: undefined,
|
||||||
|
this.flags.type === "instance"
|
||||||
|
? operator(Instances.baseUrl, identifier)
|
||||||
|
: undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import { Args } from "@oclif/core";
|
import { Args } from "@oclif/core";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import ora from "ora";
|
||||||
import { BaseCommand } from "~/cli/base";
|
import { BaseCommand } from "~/cli/base";
|
||||||
|
import { getUrl } from "~database/entities/Attachment";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
|
import { Emojis } from "~drizzle/schema";
|
||||||
|
import { config } from "~packages/config-manager";
|
||||||
|
import { MediaBackend } from "~packages/media-manager";
|
||||||
|
|
||||||
export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
|
export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
|
||||||
static override args = {
|
static override args = {
|
||||||
|
|
@ -17,7 +22,10 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
|
||||||
|
|
||||||
static override description = "Adds a new emoji";
|
static override description = "Adds a new emoji";
|
||||||
|
|
||||||
static override examples = ["<%= config.bin %> <%= command.id %>"];
|
static override examples = [
|
||||||
|
"<%= config.bin %> <%= command.id %> baba_yassie ./emojis/baba_yassie.png",
|
||||||
|
"<%= config.bin %> <%= command.id %> baba_yassie https://example.com/emojis/baba_yassie.png",
|
||||||
|
];
|
||||||
|
|
||||||
static override flags = {};
|
static override flags = {};
|
||||||
|
|
||||||
|
|
@ -26,7 +34,11 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
|
||||||
|
|
||||||
// Check if emoji already exists
|
// Check if emoji already exists
|
||||||
const existingEmoji = await db.query.Emojis.findFirst({
|
const existingEmoji = await db.query.Emojis.findFirst({
|
||||||
where: (Emojis, { eq }) => eq(Emojis.shortcode, args.shortcode),
|
where: (Emojis, { eq, and, isNull }) =>
|
||||||
|
and(
|
||||||
|
eq(Emojis.shortcode, args.shortcode),
|
||||||
|
isNull(Emojis.instanceId),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingEmoji) {
|
if (existingEmoji) {
|
||||||
|
|
@ -38,58 +50,97 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log("Placeholder command, this command is not implemented yet.");
|
let file: File | null = null;
|
||||||
|
|
||||||
/* if (!user) {
|
if (URL.canParse(args.file)) {
|
||||||
|
const spinner = ora(
|
||||||
|
`Downloading emoji from ${chalk.blue(
|
||||||
|
chalk.underline(args.file),
|
||||||
|
)}`,
|
||||||
|
).start();
|
||||||
|
|
||||||
|
const response = await fetch(args.file, {
|
||||||
|
headers: {
|
||||||
|
"Accept-Encoding": "identity",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
spinner.fail();
|
||||||
this.log(
|
this.log(
|
||||||
`${chalk.red("✗")} Failed to create user ${chalk.red(
|
`${chalk.red("✗")} Request returned status code ${chalk.red(
|
||||||
args.username,
|
response.status,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
!flags.format &&
|
const filename =
|
||||||
this.log(
|
new URL(args.file).pathname.split("/").pop() ?? "emoji";
|
||||||
`${chalk.green("✓")} Created user ${chalk.green(
|
|
||||||
user.getUser().username,
|
|
||||||
)} with id ${chalk.green(user.id)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.log(
|
file = new File([await response.blob()], filename, {
|
||||||
formatArray(
|
type:
|
||||||
[user.getUser()],
|
response.headers.get("Content-Type") ??
|
||||||
[
|
"application/octet-stream",
|
||||||
"id",
|
|
||||||
"username",
|
|
||||||
"displayName",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt",
|
|
||||||
"isAdmin",
|
|
||||||
],
|
|
||||||
flags.format as "json" | "csv" | undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!flags.format && !flags["set-password"]) {
|
|
||||||
const link = "";
|
|
||||||
|
|
||||||
this.log(
|
|
||||||
flags.format
|
|
||||||
? link
|
|
||||||
: `\nPassword reset link for ${chalk.bold(
|
|
||||||
`@${user.getUser().username}`,
|
|
||||||
)}: ${chalk.underline(chalk.blue(link))}\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const qrcode = renderUnicodeCompact(link, {
|
|
||||||
border: 2,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pad all lines of QR code with spaces
|
spinner.succeed();
|
||||||
|
} else {
|
||||||
|
const bunFile = Bun.file(args.file);
|
||||||
|
file = new File(
|
||||||
|
[await bunFile.arrayBuffer()],
|
||||||
|
args.file.split("/").pop() ?? "emoji",
|
||||||
|
{
|
||||||
|
type: bunFile.type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.log(` ${qrcode.replaceAll("\n", "\n ")}`);
|
const media = await MediaBackend.fromBackendType(
|
||||||
} */
|
config.media.backend,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
|
const spinner = ora("Uploading emoji").start();
|
||||||
|
|
||||||
|
const uploaded = await media.addFile(file).catch((e: Error) => {
|
||||||
|
spinner.fail();
|
||||||
|
this.log(`${chalk.red("✗")} Error: ${chalk.red(e.message)}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!uploaded) {
|
||||||
|
return this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.succeed();
|
||||||
|
|
||||||
|
const emoji = await db
|
||||||
|
.insert(Emojis)
|
||||||
|
.values({
|
||||||
|
shortcode: args.shortcode,
|
||||||
|
url: getUrl(uploaded.path, config),
|
||||||
|
visibleInPicker: true,
|
||||||
|
contentType: file.type,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!emoji || emoji.length === 0) {
|
||||||
|
this.log(
|
||||||
|
`${chalk.red("✗")} Failed to create emoji ${chalk.red(
|
||||||
|
args.shortcode,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(
|
||||||
|
`${chalk.green("✓")} Created emoji ${chalk.green(
|
||||||
|
args.shortcode,
|
||||||
|
)} with url ${chalk.blue(
|
||||||
|
chalk.underline(getUrl(uploaded.path, config)),
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
|
||||||
this.exit(0);
|
this.exit(0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
93
cli/commands/emoji/delete.ts
Normal file
93
cli/commands/emoji/delete.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { Args, Flags } from "@oclif/core";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { and, eq, inArray, isNull } from "drizzle-orm";
|
||||||
|
import { EmojiFinderCommand } from "~cli/classes";
|
||||||
|
import { formatArray } from "~cli/utils/format";
|
||||||
|
import { db } from "~drizzle/db";
|
||||||
|
import { Emojis } from "~drizzle/schema";
|
||||||
|
import confirm from "@inquirer/confirm";
|
||||||
|
import ora from "ora";
|
||||||
|
|
||||||
|
export default class EmojiDelete extends EmojiFinderCommand<
|
||||||
|
typeof EmojiDelete
|
||||||
|
> {
|
||||||
|
static override args = {
|
||||||
|
identifier: EmojiFinderCommand.baseArgs.identifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
static override description = "Deletes an emoji";
|
||||||
|
|
||||||
|
static override examples = [
|
||||||
|
"<%= config.bin %> <%= command.id %> baba_yassie",
|
||||||
|
'<%= config.bin %> <%= command.id %> "baba\\*" --pattern',
|
||||||
|
];
|
||||||
|
|
||||||
|
static override flags = {
|
||||||
|
confirm: Flags.boolean({
|
||||||
|
description:
|
||||||
|
"Ask for confirmation before deleting the emoji (default yes)",
|
||||||
|
allowNo: true,
|
||||||
|
default: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
const { flags, args } = await this.parse(EmojiDelete);
|
||||||
|
|
||||||
|
const emojis = await this.findEmojis();
|
||||||
|
|
||||||
|
if (!emojis || emojis.length === 0) {
|
||||||
|
this.log(chalk.bold(`${chalk.red("✗")} No emojis found`));
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display user
|
||||||
|
flags.print &&
|
||||||
|
this.log(
|
||||||
|
chalk.bold(
|
||||||
|
`${chalk.green("✓")} Found ${chalk.green(
|
||||||
|
emojis.length,
|
||||||
|
)} emoji(s)`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
flags.print &&
|
||||||
|
this.log(
|
||||||
|
formatArray(emojis, [
|
||||||
|
"id",
|
||||||
|
"shortcode",
|
||||||
|
"alt",
|
||||||
|
"contentType",
|
||||||
|
"instanceUrl",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (flags.confirm) {
|
||||||
|
const choice = await confirm({
|
||||||
|
message: `Are you sure you want to delete these emojis? ${chalk.red(
|
||||||
|
"This is irreversible.",
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!choice) {
|
||||||
|
this.log(chalk.bold(`${chalk.red("✗")} Aborted operation`));
|
||||||
|
return this.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = ora("Deleting emoji(s)").start();
|
||||||
|
|
||||||
|
await db.delete(Emojis).where(
|
||||||
|
inArray(
|
||||||
|
Emojis.id,
|
||||||
|
emojis.map((e) => e.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
spinner.succeed();
|
||||||
|
|
||||||
|
this.log(chalk.bold(`${chalk.green("✓")} Emoji(s) deleted`));
|
||||||
|
|
||||||
|
this.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
255
cli/commands/emoji/import.ts
Normal file
255
cli/commands/emoji/import.ts
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
import { Args, Flags } from "@oclif/core";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import ora from "ora";
|
||||||
|
import { BaseCommand } from "~/cli/base";
|
||||||
|
import { getUrl } from "~database/entities/Attachment";
|
||||||
|
import { db } from "~drizzle/db";
|
||||||
|
import { Emojis } from "~drizzle/schema";
|
||||||
|
import { config } from "~packages/config-manager";
|
||||||
|
import { MediaBackend } from "~packages/media-manager";
|
||||||
|
import { unzip } from "unzipit";
|
||||||
|
import { and, inArray, isNull } from "drizzle-orm";
|
||||||
|
import { lookup } from "mime-types";
|
||||||
|
|
||||||
|
type MetaType = {
|
||||||
|
emojis: {
|
||||||
|
fileName: string;
|
||||||
|
emoji: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class EmojiImport extends BaseCommand<typeof EmojiImport> {
|
||||||
|
static override args = {
|
||||||
|
path: Args.string({
|
||||||
|
description: "Path to the emoji archive (can be an URL)",
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static override description =
|
||||||
|
"Imports emojis from a zip file (which can be fetched from a zip URL, e.g. for Pleroma emoji packs)";
|
||||||
|
|
||||||
|
static override examples = [
|
||||||
|
"<%= config.bin %> <%= command.id %> https://volpeon.ink/emojis/neocat/neocat.zip",
|
||||||
|
"<%= config.bin %> <%= command.id %> export.zip",
|
||||||
|
];
|
||||||
|
|
||||||
|
static override flags = {
|
||||||
|
confirm: Flags.boolean({
|
||||||
|
description:
|
||||||
|
"Ask for confirmation before deleting the emoji (default yes)",
|
||||||
|
allowNo: true,
|
||||||
|
default: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
const { flags, args } = await this.parse(EmojiImport);
|
||||||
|
|
||||||
|
// Check if path ends in .zip, warn the user if it doesn't
|
||||||
|
if (!args.path.endsWith(".zip")) {
|
||||||
|
this.log(
|
||||||
|
`${chalk.yellow(
|
||||||
|
"⚠",
|
||||||
|
)} The path you provided does not end in .zip, this may not be a zip file. Proceeding anyway.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file: File | null = null;
|
||||||
|
|
||||||
|
if (URL.canParse(args.path)) {
|
||||||
|
const spinner = ora(
|
||||||
|
`Downloading pack from ${chalk.blue(
|
||||||
|
chalk.underline(args.path),
|
||||||
|
)}`,
|
||||||
|
).start();
|
||||||
|
|
||||||
|
const response = await fetch(args.path, {
|
||||||
|
headers: {
|
||||||
|
"Accept-Encoding": "identity",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
spinner.fail();
|
||||||
|
this.log(
|
||||||
|
`${chalk.red("✗")} Request returned status code ${chalk.red(
|
||||||
|
response.status,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename =
|
||||||
|
new URL(args.path).pathname.split("/").pop() ?? "archive";
|
||||||
|
|
||||||
|
file = new File([await response.blob()], filename, {
|
||||||
|
type:
|
||||||
|
response.headers.get("Content-Type") ??
|
||||||
|
"application/octet-stream",
|
||||||
|
});
|
||||||
|
|
||||||
|
spinner.succeed();
|
||||||
|
} else {
|
||||||
|
const bunFile = Bun.file(args.path);
|
||||||
|
file = new File(
|
||||||
|
[await bunFile.arrayBuffer()],
|
||||||
|
args.path.split("/").pop() ?? "archive",
|
||||||
|
{
|
||||||
|
type: bunFile.type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unzipSpinner = ora("Unzipping pack").start();
|
||||||
|
|
||||||
|
const { entries: unzipped } = await unzip(file);
|
||||||
|
|
||||||
|
unzipSpinner.succeed();
|
||||||
|
|
||||||
|
const entries = Object.entries(unzipped);
|
||||||
|
|
||||||
|
// Check if a meta.json file exists
|
||||||
|
const metaExists = entries.find(([name]) => name === "meta.json");
|
||||||
|
|
||||||
|
if (metaExists) {
|
||||||
|
this.log(`${chalk.green("✓")} Detected Pleroma meta.json, parsing`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = metaExists
|
||||||
|
? ((await metaExists[1].json()) as MetaType)
|
||||||
|
: ({
|
||||||
|
emojis: entries.map(([name]) => ({
|
||||||
|
fileName: name,
|
||||||
|
emoji: {
|
||||||
|
name: name.split(".")[0],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
} as MetaType);
|
||||||
|
|
||||||
|
// Get all emojis that already exist
|
||||||
|
const existingEmojis = await db
|
||||||
|
.select()
|
||||||
|
.from(Emojis)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
isNull(Emojis.instanceId),
|
||||||
|
inArray(
|
||||||
|
Emojis.shortcode,
|
||||||
|
meta.emojis.map((e) => e.emoji.name),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter out existing emojis
|
||||||
|
const newEmojis = meta.emojis.filter(
|
||||||
|
(e) => !existingEmojis.find((ee) => ee.shortcode === e.emoji.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
existingEmojis.length > 0 &&
|
||||||
|
this.log(
|
||||||
|
`${chalk.yellow("⚠")} Emojis with shortcode ${chalk.yellow(
|
||||||
|
existingEmojis.map((e) => e.shortcode).join(", "),
|
||||||
|
)} already exist in the database and will not be imported`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newEmojis.length === 0) {
|
||||||
|
this.log(`${chalk.red("✗")} No new emojis to import`);
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(
|
||||||
|
`${chalk.green("✓")} Found ${chalk.green(
|
||||||
|
newEmojis.length,
|
||||||
|
)} new emoji(s)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const importSpinner = ora("Importing emojis").start();
|
||||||
|
|
||||||
|
const media = await MediaBackend.fromBackendType(
|
||||||
|
config.media.backend,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
|
const successfullyImported: MetaType["emojis"] = [];
|
||||||
|
|
||||||
|
for (const emoji of newEmojis) {
|
||||||
|
importSpinner.text = `Uploading ${chalk.gray(emoji.emoji.name)} (${
|
||||||
|
newEmojis.indexOf(emoji) + 1
|
||||||
|
}/${newEmojis.length})`;
|
||||||
|
const zipEntry = unzipped[emoji.fileName];
|
||||||
|
|
||||||
|
if (!zipEntry) {
|
||||||
|
this.log(
|
||||||
|
`${chalk.red(
|
||||||
|
"✗",
|
||||||
|
)} Could not find file for emoji ${chalk.red(
|
||||||
|
emoji.emoji.name,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = emoji.fileName.split("/").pop() ?? "emoji";
|
||||||
|
const contentType = lookup(fileName) || "application/octet-stream";
|
||||||
|
|
||||||
|
const newFile = new File([await zipEntry.arrayBuffer()], fileName, {
|
||||||
|
type: contentType,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploaded = await media.addFile(newFile).catch((e: Error) => {
|
||||||
|
this.log(
|
||||||
|
`${chalk.red("✗")} Error uploading ${chalk.red(
|
||||||
|
emoji.emoji.name,
|
||||||
|
)}: ${chalk.red(e.message)}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!uploaded) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(Emojis)
|
||||||
|
.values({
|
||||||
|
shortcode: emoji.emoji.name,
|
||||||
|
url: getUrl(uploaded.path, config),
|
||||||
|
visibleInPicker: true,
|
||||||
|
contentType: file.type,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
successfullyImported.push(emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
importSpinner.succeed("Imported emojis");
|
||||||
|
|
||||||
|
successfullyImported.length > 0 &&
|
||||||
|
this.log(
|
||||||
|
`${chalk.green("✓")} Successfully imported ${chalk.green(
|
||||||
|
successfullyImported.length,
|
||||||
|
)} emoji(s)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
newEmojis.length - successfullyImported.length > 0 &&
|
||||||
|
this.log(
|
||||||
|
`${chalk.yellow("⚠")} Failed to import ${chalk.yellow(
|
||||||
|
newEmojis.length - successfullyImported.length,
|
||||||
|
)} emoji(s): ${chalk.yellow(
|
||||||
|
newEmojis
|
||||||
|
.filter((e) => !successfullyImported.includes(e))
|
||||||
|
.map((e) => e.emoji.name)
|
||||||
|
.join(", "),
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (successfullyImported.length === 0) {
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
70
cli/commands/emoji/list.ts
Normal file
70
cli/commands/emoji/list.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Flags } from "@oclif/core";
|
||||||
|
import { and, eq, getTableColumns, isNotNull, isNull } from "drizzle-orm";
|
||||||
|
import { BaseCommand } from "~cli/base";
|
||||||
|
import { formatArray } from "~cli/utils/format";
|
||||||
|
import { db } from "~drizzle/db";
|
||||||
|
import { Emojis, Instances } from "~drizzle/schema";
|
||||||
|
|
||||||
|
export default class EmojiList extends BaseCommand<typeof EmojiList> {
|
||||||
|
static override args = {};
|
||||||
|
|
||||||
|
static override description = "List all emojis";
|
||||||
|
|
||||||
|
static override examples = [
|
||||||
|
"<%= config.bin %> <%= command.id %> --format json --local",
|
||||||
|
"<%= config.bin %> <%= command.id %>",
|
||||||
|
];
|
||||||
|
|
||||||
|
static override flags = {
|
||||||
|
format: Flags.string({
|
||||||
|
char: "f",
|
||||||
|
description: "Output format",
|
||||||
|
options: ["json", "csv"],
|
||||||
|
}),
|
||||||
|
local: Flags.boolean({
|
||||||
|
char: "l",
|
||||||
|
description: "Local emojis only",
|
||||||
|
exclusive: ["remote"],
|
||||||
|
}),
|
||||||
|
remote: Flags.boolean({
|
||||||
|
char: "r",
|
||||||
|
description: "Remote emojis only",
|
||||||
|
exclusive: ["local"],
|
||||||
|
}),
|
||||||
|
limit: Flags.integer({
|
||||||
|
char: "n",
|
||||||
|
description: "Limit the number of emojis",
|
||||||
|
default: 200,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
const { flags } = await this.parse(EmojiList);
|
||||||
|
|
||||||
|
const emojis = await db
|
||||||
|
.select({
|
||||||
|
...getTableColumns(Emojis),
|
||||||
|
instanceUrl: Instances.baseUrl,
|
||||||
|
})
|
||||||
|
.from(Emojis)
|
||||||
|
.leftJoin(Instances, eq(Emojis.instanceId, Instances.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
flags.local ? isNull(Emojis.instanceId) : undefined,
|
||||||
|
flags.remote ? isNotNull(Emojis.instanceId) : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const keys = ["id", "shortcode", "alt", "contentType", "instanceUrl"];
|
||||||
|
|
||||||
|
this.log(
|
||||||
|
formatArray(
|
||||||
|
emojis,
|
||||||
|
keys,
|
||||||
|
flags.format as "json" | "csv" | undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -81,7 +81,7 @@ export default class UserDelete extends UserFinderCommand<typeof UserDelete> {
|
||||||
await user.delete();
|
await user.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
spinner.stop();
|
spinner.succeed();
|
||||||
|
|
||||||
this.log(chalk.bold(`${chalk.green("✓")} User(s) deleted`));
|
this.log(chalk.bold(`${chalk.green("✓")} User(s) deleted`));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import UserCreate from "./commands/user/create";
|
||||||
import UserDelete from "./commands/user/delete";
|
import UserDelete from "./commands/user/delete";
|
||||||
import UserList from "./commands/user/list";
|
import UserList from "./commands/user/list";
|
||||||
import UserReset from "./commands/user/reset";
|
import UserReset from "./commands/user/reset";
|
||||||
|
import EmojiDelete from "./commands/emoji/delete";
|
||||||
|
import EmojiList from "./commands/emoji/list";
|
||||||
|
import EmojiImport from "./commands/emoji/import";
|
||||||
|
|
||||||
// Use "explicit" oclif strategy to avoid issues with oclif's module resolver and bundling
|
// Use "explicit" oclif strategy to avoid issues with oclif's module resolver and bundling
|
||||||
export const commands = {
|
export const commands = {
|
||||||
|
|
@ -12,6 +15,9 @@ export const commands = {
|
||||||
"user:create": UserCreate,
|
"user:create": UserCreate,
|
||||||
"user:reset": UserReset,
|
"user:reset": UserReset,
|
||||||
"emoji:add": EmojiAdd,
|
"emoji:add": EmojiAdd,
|
||||||
|
"emoji:delete": EmojiDelete,
|
||||||
|
"emoji:list": EmojiList,
|
||||||
|
"emoji:import": EmojiImport,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (import.meta.path === Bun.main) {
|
if (import.meta.path === Bun.main) {
|
||||||
|
|
|
||||||
15
package.json
15
package.json
|
|
@ -93,21 +93,18 @@
|
||||||
"typescript": "^5.3.2"
|
"typescript": "^5.3.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/confirm": "^3.1.6",
|
|
||||||
"@inquirer/input": "^2.1.6",
|
|
||||||
"@oclif/core": "^3.26.6",
|
|
||||||
"cli-progress": "^3.12.0",
|
|
||||||
"ora": "^8.0.1",
|
|
||||||
"table": "^6.8.2",
|
|
||||||
"uqr": "^0.1.2",
|
|
||||||
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
||||||
"@hono/zod-validator": "^0.2.1",
|
"@hono/zod-validator": "^0.2.1",
|
||||||
|
"@inquirer/confirm": "^3.1.6",
|
||||||
|
"@inquirer/input": "^2.1.6",
|
||||||
"@json2csv/plainjs": "^7.0.6",
|
"@json2csv/plainjs": "^7.0.6",
|
||||||
|
"@oclif/core": "^3.26.6",
|
||||||
"@tufjs/canonical-json": "^2.0.0",
|
"@tufjs/canonical-json": "^2.0.0",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"bullmq": "^5.7.1",
|
"bullmq": "^5.7.1",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"cli-parser": "workspace:*",
|
"cli-parser": "workspace:*",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
"cli-table": "^0.3.11",
|
"cli-table": "^0.3.11",
|
||||||
"config-manager": "workspace:*",
|
"config-manager": "workspace:*",
|
||||||
"drizzle-orm": "^0.30.7",
|
"drizzle-orm": "^0.30.7",
|
||||||
|
|
@ -131,11 +128,15 @@
|
||||||
"meilisearch": "^0.39.0",
|
"meilisearch": "^0.39.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"oauth4webapi": "^2.4.0",
|
"oauth4webapi": "^2.4.0",
|
||||||
|
"ora": "^8.0.1",
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"qs": "^6.12.1",
|
"qs": "^6.12.1",
|
||||||
"sharp": "^0.33.3",
|
"sharp": "^0.33.3",
|
||||||
"string-comparison": "^1.3.0",
|
"string-comparison": "^1.3.0",
|
||||||
"stringify-entities": "^4.0.4",
|
"stringify-entities": "^4.0.4",
|
||||||
|
"table": "^6.8.2",
|
||||||
|
"unzipit": "^1.4.3",
|
||||||
|
"uqr": "^0.1.2",
|
||||||
"xss": "^1.0.15",
|
"xss": "^1.0.15",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zod-validation-error": "^3.2.0"
|
"zod-validation-error": "^3.2.0"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue