Plugin
CustomCommands
Allows you to create custom slash commands / tags
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import "./styles.css";8
9
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage } from "@api/Commands";10
import { migratePluginSettings } from "@api/Settings";11
import { Devs } from "@utils/constants";12
import { sendMessage } from "@utils/discord";13
import definePlugin from "@utils/types";14
import { FluxDispatcher, MessageActions, PendingReplyStore } from "@webpack/common";15
16
import { openCreateTagModal } from "./CreateTagModal";17
import { getTag, getTags, removeTag, settings, Tag } from "./settings";18
19
const CustomCommandsMarker = Symbol("CustomCommands");20
const ArgumentRegex = /{{(.+?)}}/g;21
22
export 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
37
export 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 === null50
})),51
{52
name: "ephemeral",53
description: "Whether the response should only be visible to you",54
type: ApplicationCommandOptionType.BOOLEAN,55
required: false56
}57
],58
59
execute: async (args, { channel }) => {60
const ephemeral = findOption(args, "ephemeral", false);61
62
const response = tag.message63
.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
78
migratePluginSettings("CustomCommands", "MessageTags");79
export 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: true121
}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