Skip to content

Commit 0153317

Browse files
feat: embed commands
1 parent 7c56cd5 commit 0153317

File tree

10 files changed

+393
-41
lines changed

10 files changed

+393
-41
lines changed

build_src/src/main/typescript/tasks/RunTask.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ class RunTask extends AbstractTask {
99
@TaskAction
1010
protected override run() {
1111
setTimeout(async () => {
12-
let code = -1;
12+
let code: number;
13+
const argv = process.argv.slice(process.argv.indexOf('--') + 1);
1314

1415
if (process.argv.includes("--node")) {
1516
await this.blaze.taskManager.executeTask("build");
1617
code =
17-
spawnSync("node", [`${process.cwd()}/build/out/main/typescript/index.js`], {
18+
spawnSync("node", [`${process.cwd()}/build/out/main/typescript/index.js`, ...argv], {
1819
stdio: "inherit"
1920
}).status ?? -1;
2021
} else {
2122
code =
22-
spawnSync("bun", [`${process.cwd()}/src/main/typescript/bun.ts`], {
23+
spawnSync("bun", [`${process.cwd()}/src/main/typescript/bun.ts`, ...argv], {
2324
stdio: "inherit"
2425
}).status ?? -1;
2526
}

src/framework/typescript/commands/Context.ts

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -62,27 +62,9 @@ abstract class Context<T extends CommandMessage = CommandMessage> {
6262
public readonly commandMessage: T;
6363
public abstract readonly type: ContextType;
6464

65-
public isLegacy(): this is LegacyContext {
66-
return this.type === ContextType.Legacy;
67-
}
68-
69-
public isChatInput(): this is InteractionContext<ChatInputCommandInteraction> {
70-
return this.type === ContextType.ChatInput;
71-
}
72-
73-
public isContextMenu(): this is InteractionContext<ContextMenuCommandInteraction> {
74-
return (
75-
this.type === ContextType.MessageContextMenu ||
76-
this.type === ContextType.UserContextMenu
77-
);
78-
}
79-
80-
public isMessageContextMenu(): this is InteractionContext<MessageContextMenuCommandInteraction> {
81-
return this.type === ContextType.MessageContextMenu;
82-
}
83-
84-
public isUserContextMenu(): this is InteractionContext<UserContextMenuCommandInteraction> {
85-
return this.type === ContextType.UserContextMenu;
65+
public constructor(commandName: string, commandMessage: T) {
66+
this.commandName = commandName;
67+
this.commandMessage = commandMessage;
8668
}
8769

8870
public get guildId(): Snowflake {
@@ -118,11 +100,36 @@ abstract class Context<T extends CommandMessage = CommandMessage> {
118100
}
119101

120102
public abstract get userId(): Snowflake;
103+
121104
public abstract get user(): User;
122105

123-
public constructor(commandName: string, commandMessage: T) {
124-
this.commandName = commandName;
125-
this.commandMessage = commandMessage;
106+
public get attachments(): Collection<Snowflake, Attachment> {
107+
return this.commandMessage instanceof Message
108+
? this.commandMessage.attachments
109+
: new Collection();
110+
}
111+
112+
public isLegacy(): this is LegacyContext {
113+
return this.type === ContextType.Legacy;
114+
}
115+
116+
public isChatInput(): this is InteractionContext<ChatInputCommandInteraction> {
117+
return this.type === ContextType.ChatInput;
118+
}
119+
120+
public isContextMenu(): this is InteractionContext<ContextMenuCommandInteraction> {
121+
return (
122+
this.type === ContextType.MessageContextMenu ||
123+
this.type === ContextType.UserContextMenu
124+
);
125+
}
126+
127+
public isMessageContextMenu(): this is InteractionContext<MessageContextMenuCommandInteraction> {
128+
return this.type === ContextType.MessageContextMenu;
129+
}
130+
131+
public isUserContextMenu(): this is InteractionContext<UserContextMenuCommandInteraction> {
132+
return this.type === ContextType.UserContextMenu;
126133
}
127134

128135
public reply(options: Parameters<this["commandMessage"]["reply"]>[0]): Promise<Message> {
@@ -146,12 +153,6 @@ abstract class Context<T extends CommandMessage = CommandMessage> {
146153
return this.reply({ embeds });
147154
}
148155

149-
public get attachments(): Collection<Snowflake, Attachment> {
150-
return this.commandMessage instanceof Message
151-
? this.commandMessage.attachments
152-
: new Collection();
153-
}
154-
155156
public async defer(options?: InteractionDeferReplyOptions) {
156157
if (this.commandMessage instanceof Message) {
157158
return;
@@ -167,7 +168,7 @@ abstract class Context<T extends CommandMessage = CommandMessage> {
167168
public async error(options: Parameters<this["commandMessage"]["reply"]>[0]) {
168169
return this.reply(
169170
typeof options === "string"
170-
? `${this.emoji("error")} ${options}`
171+
? `${this.emoji("error") ?? ""} ${options}`
171172
: {
172173
...(options as unknown as MessageCreateOptions & InteractionReplyOptions),
173174
ephemeral: true,
@@ -181,7 +182,7 @@ abstract class Context<T extends CommandMessage = CommandMessage> {
181182
public async success(options: Parameters<this["commandMessage"]["reply"]>[0]) {
182183
return this.reply(
183184
typeof options === "string"
184-
? `${this.emoji("check")} ${options}`
185+
? `${this.emoji("check") ?? ""} ${options}`
185186
: {
186187
...(options as unknown as MessageCreateOptions & InteractionReplyOptions),
187188
ephemeral: true,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ChannelType } from "discord.js";
2+
3+
export const TextableChannelTypes = [
4+
ChannelType.GuildText,
5+
ChannelType.GuildAnnouncement,
6+
ChannelType.GuildVoice,
7+
ChannelType.AnnouncementThread,
8+
ChannelType.PublicThread,
9+
ChannelType.PrivateThread
10+
];

src/main/typescript/commands/settings/SystemStatusCommand.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class SystemStatusCommand extends Command {
3232
message = await context.reply({
3333
embeds: [
3434
{
35-
description: `${context.emoji("loading")} Checking system status...`,
35+
description: `${context.emoji("loading")??''} Checking system status...`,
3636
color: Colors.Primary
3737
}
3838
]
@@ -102,10 +102,10 @@ class SystemStatusCommand extends Command {
102102
}
103103

104104
const embed = {
105-
description: `## ${context.emoji("sudobot")} System Status\n${
105+
description: `## ${context.emoji("sudobot")??''} System Status\n${
106106
status === "degraded"
107107
? "⚠️"
108-
: context.emoji(status === "operational" ? "check" : "error")
108+
: context.emoji(status === "operational" ? "check" : "error")??''
109109
} ${
110110
status === "operational"
111111
? "All systems are operational"
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Command } from "@framework/commands/Command";
2+
import { PermissionFlags } from "@framework/permissions/PermissionFlag";
3+
import type LegacyContext from "@framework/commands/LegacyContext";
4+
import type InteractionContext from "@framework/commands/InteractionContext";
5+
import {
6+
type ChatInputCommandInteraction,
7+
EmbedBuilder,
8+
type GuildBasedChannel
9+
} from "discord.js";
10+
import JSON5 from "json5";
11+
import { TakesArgument } from "@framework/arguments/ArgumentTypes";
12+
import RestStringArgument from "@framework/arguments/RestStringArgument";
13+
import { ErrorType } from "@framework/arguments/InvalidArgumentError";
14+
import { z } from "zod";
15+
16+
const EmbedZodSchema = z.object({
17+
title: z.string().optional(),
18+
description: z.string().optional(),
19+
color: z.number().optional(),
20+
fields: z.array(
21+
z.object({
22+
name: z.string(),
23+
value: z.string(),
24+
inline: z.boolean().default(false)
25+
})
26+
),
27+
footer: z
28+
.object({
29+
text: z.string()
30+
})
31+
.optional(),
32+
image: z
33+
.object({
34+
url: z.string()
35+
})
36+
.optional(),
37+
thumbnail: z
38+
.object({
39+
url: z.string()
40+
})
41+
.optional(),
42+
author: z
43+
.object({
44+
name: z.string(),
45+
icon_url: z.string().optional()
46+
})
47+
.optional(),
48+
timestamp: z.string().optional(),
49+
url: z.string().optional(),
50+
video: z
51+
.object({
52+
url: z.string()
53+
})
54+
.optional()
55+
});
56+
57+
type EmbedBuildCommandArgs = {
58+
schema: string;
59+
};
60+
61+
@TakesArgument<EmbedBuildCommandArgs>({
62+
names: ["schema"],
63+
types: [RestStringArgument],
64+
optional: false,
65+
errorMessages: [
66+
{
67+
[ErrorType.Required]: "Please provide an embed schema."
68+
}
69+
]
70+
})
71+
class EmbedBuildCommand extends Command {
72+
public override readonly name: string = "embed::build";
73+
public override readonly description: string = "Generate an embed from a schema.";
74+
public override readonly usage = ["<schema: String>"];
75+
public override readonly permissions = [
76+
PermissionFlags.ManageGuild,
77+
PermissionFlags.ManageMessages
78+
];
79+
public override readonly permissionCheckingMode = "or";
80+
81+
public override async execute(
82+
context: LegacyContext | InteractionContext<ChatInputCommandInteraction>,
83+
args: EmbedBuildCommandArgs
84+
) {
85+
await context.defer({
86+
ephemeral: true
87+
});
88+
89+
let embed: EmbedBuilder;
90+
91+
try {
92+
const parsed = await EmbedZodSchema.parseAsync(JSON5.parse(args.schema));
93+
embed = new EmbedBuilder(parsed);
94+
} catch (error) {
95+
this.application.logger.debug(error);
96+
97+
if (error instanceof z.ZodError) {
98+
await context.error(
99+
`Invalid embed schema: ${error.errors.map(e => e.message).join(", ")}`
100+
);
101+
} else {
102+
await context.error("Invalid schema provided.");
103+
}
104+
105+
return;
106+
}
107+
108+
const channel = context.isChatInput()
109+
? (context.options.getChannel("channel") as GuildBasedChannel) ?? context.channel
110+
: context.channel;
111+
112+
if (!channel?.isTextBased()) {
113+
await context.error("This command can only be used in text channels.");
114+
return;
115+
}
116+
117+
await channel.send({
118+
embeds: [embed]
119+
});
120+
121+
if (context.isChatInput()) {
122+
await context.reply({ content: "Message sent." });
123+
}
124+
}
125+
}
126+
127+
export default EmbedBuildCommand;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import AbstractRootCommand from "@framework/commands/AbstractRootCommand";
2+
import { PermissionFlags } from "@framework/permissions/PermissionFlag";
3+
import type { Buildable } from "@framework/commands/Command";
4+
import { ChannelType, type SlashCommandSubcommandBuilder } from "discord.js";
5+
import { TextableChannelTypes } from "@framework/utils/channel";
6+
7+
class EmbedCommand extends AbstractRootCommand {
8+
public override readonly name: string = "embed";
9+
public override readonly description: string = "Create and manage embeds.";
10+
public override readonly usage = ["<subcommand: String> [...args: String[]]"];
11+
public override readonly permissions = [
12+
PermissionFlags.ManageGuild,
13+
PermissionFlags.ManageMessages
14+
];
15+
public override readonly permissionCheckingMode = "or";
16+
public override readonly isolatedSubcommands = true;
17+
public override readonly aliases = ["embeds"];
18+
public override readonly subcommands = ["send", "schema", "build"];
19+
20+
public override build(): Buildable[] {
21+
return [
22+
this.buildChatInput()
23+
.addSubcommand(subcommand =>
24+
this.buildEmbedOptions(
25+
subcommand.setName("send").setDescription("Send an embed.")
26+
).addChannelOption(option =>
27+
option
28+
.setName("channel")
29+
.setDescription("The channel to send the embed in")
30+
.addChannelTypes(...(TextableChannelTypes as ChannelType.GuildText[]))
31+
)
32+
)
33+
.addSubcommand(subcommand =>
34+
this.buildEmbedOptions(
35+
subcommand
36+
.setName("schema")
37+
.setDescription("Create an embed schema for later use.")
38+
)
39+
)
40+
.addSubcommand(subcommand =>
41+
subcommand
42+
.setName("build")
43+
.setDescription("Build and send an embed using a schema.")
44+
.addStringOption(option =>
45+
option
46+
.setName("schema")
47+
.setDescription("The schema to use")
48+
.setRequired(true)
49+
)
50+
.addChannelOption(option =>
51+
option
52+
.setName("channel")
53+
.setDescription("The channel to send the embed in")
54+
.addChannelTypes(
55+
...(TextableChannelTypes as ChannelType.GuildText[])
56+
)
57+
)
58+
)
59+
];
60+
}
61+
62+
private buildEmbedOptions(builder: SlashCommandSubcommandBuilder) {
63+
return builder
64+
.addStringOption(option =>
65+
option.setName("author_name").setDescription("The embed author name")
66+
)
67+
.addStringOption(option =>
68+
option.setName("author_icon_url").setDescription("The embed author icon URL")
69+
)
70+
.addStringOption(option => option.setName("title").setDescription("The embed title"))
71+
.addStringOption(option =>
72+
option.setName("description").setDescription("The embed description")
73+
)
74+
.addStringOption(option =>
75+
option.setName("thumbnail").setDescription("The embed thumbnail URL")
76+
)
77+
.addStringOption(option =>
78+
option.setName("image").setDescription("The embed image attachment URL")
79+
)
80+
.addStringOption(option =>
81+
option.setName("video").setDescription("The embed video attachment URL")
82+
)
83+
.addStringOption(option =>
84+
option.setName("footer_text").setDescription("The embed footer text")
85+
)
86+
.addStringOption(option =>
87+
option.setName("footer_icon_url").setDescription("The embed footer icon URL")
88+
)
89+
.addStringOption(option =>
90+
option
91+
.setName("timestamp")
92+
.setDescription("The embed timestamp, use 'current' to set current date")
93+
)
94+
.addStringOption(option =>
95+
option.setName("color").setDescription("The embed color (default is #007bff)")
96+
)
97+
.addStringOption(option => option.setName("url").setDescription("The embed URL"))
98+
.addStringOption(option =>
99+
option
100+
.setName("fields")
101+
.setDescription(
102+
"The embed fields, should be in `Field 1: Value 1, Field 2: Value 2` format"
103+
)
104+
);
105+
}
106+
}
107+
108+
export default EmbedCommand;

0 commit comments

Comments
 (0)