Plugin

CustomCommands

Allows you to create custom slash commands / tags

Commands Customisation Utility
index.ts
Download

Source

src/plugins/customCommands/index.ts
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import "./styles.css";
8
9import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage } from "@api/Commands";
10import { migratePluginSettings } from "@api/Settings";
11import { Devs } from "@utils/constants";
12import { sendMessage } from "@utils/discord";
13import definePlugin from "@utils/types";
14import { FluxDispatcher, MessageActions, PendingReplyStore } from "@webpack/common";
15
16import { openCreateTagModal } from "./CreateTagModal";
17import { getTag, getTags, removeTag, settings, Tag } from "./settings";
18
19const CustomCommandsMarker = Symbol("CustomCommands");
20const ArgumentRegex = /{{(.+?)}}/g;
21
22export function parseTagArguments(message: string) {
23 const args = [] as { name: string, defaultValue: string | null; }[];
24
25 for (const [, value] of message.matchAll(ArgumentRegex)) {
26 const [name, defaultValue] = value.split("=").map(s => s.trim());
27
28 if (!name) continue;
29 if (args.some(arg => arg.name === name)) continue;
30
31 args.push({ name: name.toLowerCase(), defaultValue: defaultValue ?? null });
32 }
33
34 return args;
35}
36
37export function registerTagCommand(tag: Tag) {
38 const tagArguments = parseTagArguments(tag.message);
39
40 registerCommand({
41 name: tag.name,
42 description: tag.name,
43 inputType: ApplicationCommandInputType.BUILT_IN,
44 options: [
45 ...tagArguments.map(arg => ({
46 name: arg.name,
47 description: arg.name,
48 type: ApplicationCommandOptionType.STRING,
49 required: arg.defaultValue === null
50 })),
51 {
52 name: "ephemeral",
53 description: "Whether the response should only be visible to you",
54 type: ApplicationCommandOptionType.BOOLEAN,
55 required: false
56 }
57 ],
58
59 execute: async (args, { channel }) => {
60 const ephemeral = findOption(args, "ephemeral", false);
61
62 const response = tag.message
63 .replace(ArgumentRegex, (fullMatch, value: string) => {
64 const [argName, defaultValue] = value.split("=").map(s => s.trim());
65 return findOption(args, argName, null) ?? defaultValue ?? fullMatch;
66 })
67 .replaceAll("\\n", "\n");
68
69 const doSend = ephemeral ? sendBotMessage : sendMessage;
70 doSend(channel.id, { content: response }, false, MessageActions.getSendMessageOptionsForReply(PendingReplyStore.getPendingReply(channel.id)));
71 FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId: channel.id });
72 },
73 [CustomCommandsMarker]: true,
74 }, "CustomCommands");
75}
76
77
78migratePluginSettings("CustomCommands", "MessageTags");
79export default definePlugin({
80 name: "CustomCommands",
81 description: "Allows you to create custom slash commands / tags",
82 searchTerms: ["MessageTags"],
83 authors: [Devs.Ven, Devs.Luna,],
84 tags: ["Commands", "Customisation", "Utility"],
85 settings,
86
87 async start() {
88 const tags = getTags();
89 for (const tagName in tags) {
90 registerTagCommand(tags[tagName]);
91 }
92 },
93
94 commands: [
95 {
96 name: "tags",
97 description: "Manage all custom commands",
98 inputType: ApplicationCommandInputType.BUILT_IN,
99 options: [
100 {
101 name: "create",
102 description: "Create a new tag",
103 type: ApplicationCommandOptionType.SUB_COMMAND,
104 },
105 {
106 name: "list",
107 description: "List all your tags",
108 type: ApplicationCommandOptionType.SUB_COMMAND,
109 options: []
110 },
111 {
112 name: "delete",
113 description: "Remove a tag by name",
114 type: ApplicationCommandOptionType.SUB_COMMAND,
115 options: [
116 {
117 name: "tag-name",
118 description: "The name of the tag",
119 type: ApplicationCommandOptionType.STRING,
120 required: true
121 }
122 ]
123 },
124 ],
125
126 async execute(args, ctx) {
127 switch (args[0].name) {
128 case "create": {
129 openCreateTagModal();
130 break;
131 }
132
133 case "delete": {
134 const name: string = findOption(args[0].options, "tag-name", "");
135
136 if (!getTag(name))
137 return sendBotMessage(ctx.channel.id, {
138 content: `A Tag with the name **${name}** does not exist!`
139 });
140
141 removeTag(name);
142
143 sendBotMessage(ctx.channel.id, {
144 content: `Successfully deleted the tag **${name}**!`
145 });
146
147 break;
148 }
149
150 case "list": {
151 const content = Object.values(getTags())
152 .map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`)
153 .join("\n");
154
155 sendBotMessage(ctx.channel.id, {
156 content: content || "Woops! There are no tags yet, use `/tags create` to create one!",
157 });
158
159 break;
160 }
161 }
162 }
163 }
164 ]
165});
166