mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
Button role improvements, proper validation
This commit is contained in:
parent
7bf5e1f3c6
commit
653d6c1dc2
5 changed files with 166 additions and 19 deletions
|
@ -1,5 +1,8 @@
|
||||||
import { PluginOptions } from "knub";
|
import { PluginOptions } from "knub";
|
||||||
import { GuildButtonRoles } from "src/data/GuildButtonRoles";
|
import { ConfigPreprocessorFn } from "knub/dist/config/configTypes";
|
||||||
|
import { GuildButtonRoles } from "../../data/GuildButtonRoles";
|
||||||
|
import { isValidSnowflake } from "../../utils";
|
||||||
|
import { StrictValidationError } from "../../validatorUtils";
|
||||||
import { GuildReactionRoles } from "../../data/GuildReactionRoles";
|
import { GuildReactionRoles } from "../../data/GuildReactionRoles";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||||
import { Queue } from "../../Queue";
|
import { Queue } from "../../Queue";
|
||||||
|
@ -14,6 +17,7 @@ import { ButtonInteractionEvt } from "./events/ButtonInteractionEvt";
|
||||||
import { MessageDeletedEvt } from "./events/MessageDeletedEvt";
|
import { MessageDeletedEvt } from "./events/MessageDeletedEvt";
|
||||||
import { ConfigSchema, ReactionRolesPluginType } from "./types";
|
import { ConfigSchema, ReactionRolesPluginType } from "./types";
|
||||||
import { autoRefreshLoop } from "./util/autoRefreshLoop";
|
import { autoRefreshLoop } from "./util/autoRefreshLoop";
|
||||||
|
import { getRowCount } from "./util/splitButtonsIntoRows";
|
||||||
|
|
||||||
const MIN_AUTO_REFRESH = 1000 * 60 * 15; // 15min minimum, let's not abuse the API
|
const MIN_AUTO_REFRESH = 1000 * 60 * 15; // 15min minimum, let's not abuse the API
|
||||||
|
|
||||||
|
@ -36,6 +40,57 @@ const defaultOptions: PluginOptions<ReactionRolesPluginType> = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAXIMUM_COMPONENT_ROWS = 5;
|
||||||
|
|
||||||
|
const configPreprocessor: ConfigPreprocessorFn<ReactionRolesPluginType> = options => {
|
||||||
|
if (options.config.button_groups) {
|
||||||
|
for (const [groupName, group] of Object.entries(options.config.button_groups)) {
|
||||||
|
const defaultButtonNames = Object.keys(group.default_buttons);
|
||||||
|
const defaultButtons = Object.values(group.default_buttons);
|
||||||
|
const menuNames = Object.keys(group.button_menus ?? []);
|
||||||
|
|
||||||
|
const defaultBtnRowCount = getRowCount(defaultButtons);
|
||||||
|
if (defaultBtnRowCount > MAXIMUM_COMPONENT_ROWS || defaultBtnRowCount === 0) {
|
||||||
|
throw new StrictValidationError([
|
||||||
|
`Invalid row count for default_buttons: You currently have ${defaultBtnRowCount}, the maximum is 5. A new row is started automatically each 5 consecutive buttons.`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < defaultButtons.length; i++) {
|
||||||
|
const defBtn = defaultButtons[i];
|
||||||
|
if (!menuNames.includes(defBtn.role_or_menu) && !isValidSnowflake(defBtn.role_or_menu)) {
|
||||||
|
throw new StrictValidationError([
|
||||||
|
`Invalid value for default_buttons/${defaultButtonNames[i]}/role_or_menu: ${defBtn.role_or_menu} is neither an existing menu nor a valid snowflake.`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [menuName, menuButtonEntries] of Object.entries(group.button_menus ?? [])) {
|
||||||
|
const menuButtonNames = Object.keys(menuButtonEntries);
|
||||||
|
const menuButtons = Object.values(menuButtonEntries);
|
||||||
|
|
||||||
|
const menuButtonRowCount = getRowCount(menuButtons);
|
||||||
|
if (menuButtonRowCount > MAXIMUM_COMPONENT_ROWS || menuButtonRowCount === 0) {
|
||||||
|
throw new StrictValidationError([
|
||||||
|
`Invalid row count for button_menus/${menuName}: You currently have ${menuButtonRowCount}, the maximum is 5. A new row is started automatically each 5 consecutive buttons.`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < menuButtons.length; i++) {
|
||||||
|
const menuBtn = menuButtons[i];
|
||||||
|
if (!menuNames.includes(menuBtn.role_or_menu) && !isValidSnowflake(menuBtn.role_or_menu)) {
|
||||||
|
throw new StrictValidationError([
|
||||||
|
`Invalid value for button_menus/${menuButtonNames[i]}/role_or_menu: ${menuBtn.role_or_menu} is neither an existing menu nor a valid snowflake.`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
export const ReactionRolesPlugin = zeppelinGuildPlugin<ReactionRolesPluginType>()({
|
export const ReactionRolesPlugin = zeppelinGuildPlugin<ReactionRolesPluginType>()({
|
||||||
name: "reaction_roles",
|
name: "reaction_roles",
|
||||||
showInDocs: true,
|
showInDocs: true,
|
||||||
|
@ -61,6 +116,7 @@ export const ReactionRolesPlugin = zeppelinGuildPlugin<ReactionRolesPluginType>(
|
||||||
ButtonInteractionEvt,
|
ButtonInteractionEvt,
|
||||||
MessageDeletedEvt,
|
MessageDeletedEvt,
|
||||||
],
|
],
|
||||||
|
configPreprocessor,
|
||||||
|
|
||||||
beforeLoad(pluginData) {
|
beforeLoad(pluginData) {
|
||||||
const { state, guild } = pluginData;
|
const { state, guild } = pluginData;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||||
import { reactionRolesCmd } from "../types";
|
import { reactionRolesCmd } from "../types";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { splitButtonsIntoRows } from "../util/splitButtonsIntoRows";
|
||||||
|
|
||||||
export const PostButtonRolesCmd = reactionRolesCmd({
|
export const PostButtonRolesCmd = reactionRolesCmd({
|
||||||
trigger: "reaction_roles post",
|
trigger: "reaction_roles post",
|
||||||
|
@ -16,6 +17,10 @@ export const PostButtonRolesCmd = reactionRolesCmd({
|
||||||
|
|
||||||
async run({ message: msg, args, pluginData }) {
|
async run({ message: msg, args, pluginData }) {
|
||||||
const cfg = pluginData.config.get();
|
const cfg = pluginData.config.get();
|
||||||
|
if (!cfg.button_groups) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, "No button groups defined in config");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const group = cfg.button_groups[args.buttonGroup];
|
const group = cfg.button_groups[args.buttonGroup];
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
|
@ -31,10 +36,11 @@ export const PostButtonRolesCmd = reactionRolesCmd({
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
const btn = new MessageButton()
|
const btn = new MessageButton()
|
||||||
.setLabel(button.label)
|
.setLabel(button.label ?? "")
|
||||||
.setStyle("PRIMARY")
|
.setStyle(button.style ?? "PRIMARY")
|
||||||
.setType("BUTTON")
|
.setType("BUTTON")
|
||||||
.setCustomID(customId);
|
.setCustomID(customId)
|
||||||
|
.setDisabled(button.disabled ?? false);
|
||||||
|
|
||||||
if (button.emoji) {
|
if (button.emoji) {
|
||||||
const emo = pluginData.client.emojis.resolve(button.emoji) ?? button.emoji;
|
const emo = pluginData.client.emojis.resolve(button.emoji) ?? button.emoji;
|
||||||
|
@ -44,10 +50,10 @@ export const PostButtonRolesCmd = reactionRolesCmd({
|
||||||
buttons.push(btn);
|
buttons.push(btn);
|
||||||
toInsert.push({ customId, buttonGroup: args.buttonGroup, buttonName });
|
toInsert.push({ customId, buttonGroup: args.buttonGroup, buttonName });
|
||||||
}
|
}
|
||||||
const row = new MessageActionRow().addComponents(buttons);
|
const rows = splitButtonsIntoRows(buttons, Object.values(group.default_buttons)); // new MessageActionRow().addComponents(buttons);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newMsg = await args.channel.send({ content: group.message, components: [row], split: false });
|
const newMsg = await args.channel.send({ content: group.message, components: rows, split: false });
|
||||||
|
|
||||||
for (const btn of toInsert) {
|
for (const btn of toInsert) {
|
||||||
await pluginData.state.buttonRoles.add(
|
await pluginData.state.buttonRoles.add(
|
||||||
|
|
|
@ -1,21 +1,35 @@
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub";
|
import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub";
|
||||||
import { GuildButtonRoles } from "src/data/GuildButtonRoles";
|
import { GuildButtonRoles } from "src/data/GuildButtonRoles";
|
||||||
|
import { tNullable } from "../../utils";
|
||||||
import { GuildReactionRoles } from "../../data/GuildReactionRoles";
|
import { GuildReactionRoles } from "../../data/GuildReactionRoles";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||||
import { Queue } from "../../Queue";
|
import { Queue } from "../../Queue";
|
||||||
|
|
||||||
|
// These need to be updated every time discord adds/removes a style,
|
||||||
|
// but i cant figure out how to import MessageButtonStyles at runtime
|
||||||
|
enum ButtonStyles {
|
||||||
|
PRIMARY = 1,
|
||||||
|
SECONDARY = 2,
|
||||||
|
SUCCESS = 3,
|
||||||
|
DANGER = 4,
|
||||||
|
// LINK = 5, We do not want users to create link buttons, but it would be style 5
|
||||||
|
}
|
||||||
|
|
||||||
const ButtonOpts = t.type({
|
const ButtonOpts = t.type({
|
||||||
label: t.string,
|
label: tNullable(t.string),
|
||||||
emoji: t.string,
|
emoji: tNullable(t.string),
|
||||||
role_or_menu: t.string,
|
role_or_menu: t.string,
|
||||||
|
style: tNullable(t.keyof(ButtonStyles)), // https://discord.js.org/#/docs/main/master/typedef/MessageButtonStyle
|
||||||
|
disabled: tNullable(t.boolean),
|
||||||
|
end_row: tNullable(t.boolean),
|
||||||
});
|
});
|
||||||
export type TButtonOpts = t.TypeOf<typeof ButtonOpts>;
|
export type TButtonOpts = t.TypeOf<typeof ButtonOpts>;
|
||||||
|
|
||||||
const ButtonPairOpts = t.type({
|
const ButtonPairOpts = t.type({
|
||||||
message: t.string,
|
message: t.string,
|
||||||
default_buttons: t.record(t.string, ButtonOpts),
|
default_buttons: t.record(t.string, ButtonOpts),
|
||||||
button_menus: t.record(t.string, t.record(t.string, ButtonOpts)),
|
button_menus: tNullable(t.record(t.string, t.record(t.string, ButtonOpts))),
|
||||||
});
|
});
|
||||||
export type TButtonPairOpts = t.TypeOf<typeof ButtonPairOpts>;
|
export type TButtonPairOpts = t.TypeOf<typeof ButtonPairOpts>;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { LogType } from "../../../data/LogType";
|
||||||
import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin";
|
import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin";
|
||||||
import { ReactionRolesPluginType, TButtonPairOpts } from "../types";
|
import { ReactionRolesPluginType, TButtonPairOpts } from "../types";
|
||||||
import { generateStatelessCustomId } from "./buttonCustomIdFunctions";
|
import { generateStatelessCustomId } from "./buttonCustomIdFunctions";
|
||||||
|
import { splitButtonsIntoRows } from "./splitButtonsIntoRows";
|
||||||
|
|
||||||
export async function handleOpenMenu(
|
export async function handleOpenMenu(
|
||||||
pluginData: GuildPluginData<ReactionRolesPluginType>,
|
pluginData: GuildPluginData<ReactionRolesPluginType>,
|
||||||
|
@ -12,14 +13,29 @@ export async function handleOpenMenu(
|
||||||
context,
|
context,
|
||||||
) {
|
) {
|
||||||
const menuButtons: MessageButton[] = [];
|
const menuButtons: MessageButton[] = [];
|
||||||
|
if (group.button_menus == null) {
|
||||||
|
await int.reply({
|
||||||
|
content: `A configuration error was encountered, please contact the Administrators!`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
pluginData
|
||||||
|
.getPlugin(LogsPlugin)
|
||||||
|
.log(
|
||||||
|
LogType.BOT_ALERT,
|
||||||
|
`**A configuration error occured** on buttons for message ${int.message.id}, no menus found in config`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const menuButton of Object.values(group.button_menus[context.roleOrMenu])) {
|
for (const menuButton of Object.values(group.button_menus[context.roleOrMenu])) {
|
||||||
const customId = await generateStatelessCustomId(pluginData, context.groupName, menuButton.role_or_menu);
|
const customId = await generateStatelessCustomId(pluginData, context.groupName, menuButton.role_or_menu);
|
||||||
|
|
||||||
const btn = new MessageButton()
|
const btn = new MessageButton()
|
||||||
.setLabel(menuButton.label)
|
.setLabel(menuButton.label ?? "")
|
||||||
.setStyle("PRIMARY")
|
.setStyle("PRIMARY")
|
||||||
.setType("BUTTON")
|
.setType("BUTTON")
|
||||||
.setCustomID(customId);
|
.setCustomID(customId)
|
||||||
|
.setDisabled(menuButton.disabled ?? false);
|
||||||
|
|
||||||
if (menuButton.emoji) {
|
if (menuButton.emoji) {
|
||||||
const emo = pluginData.client.emojis.resolve(menuButton.emoji) ?? menuButton.emoji;
|
const emo = pluginData.client.emojis.resolve(menuButton.emoji) ?? menuButton.emoji;
|
||||||
|
@ -41,9 +57,9 @@ export async function handleOpenMenu(
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const row = new MessageActionRow().addComponents(menuButtons);
|
const rows = splitButtonsIntoRows(menuButtons, Object.values(group.button_menus[context.roleOrMenu])); // new MessageActionRow().addComponents(menuButtons);
|
||||||
|
|
||||||
int.reply({ content: `Click to add/remove a role`, components: [row], ephemeral: true });
|
int.reply({ content: `Click to add/remove a role`, components: rows, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,12 +85,26 @@ export async function handleModifyRole(
|
||||||
}
|
}
|
||||||
|
|
||||||
const member = await pluginData.guild.members.fetch(int.user.id);
|
const member = await pluginData.guild.members.fetch(int.user.id);
|
||||||
if (member.roles.cache.has(role.id)) {
|
try {
|
||||||
await member.roles.remove(role, `Button Roles on message ${int.message.id}`);
|
if (member.roles.cache.has(role.id)) {
|
||||||
await int.reply({ content: `Role **${role.name}** removed`, ephemeral: true });
|
await member.roles.remove(role, `Button Roles on message ${int.message.id}`);
|
||||||
} else {
|
await int.reply({ content: `Role **${role.name}** removed`, ephemeral: true });
|
||||||
await member.roles.add(role, `Button Roles on message ${int.message.id}`);
|
} else {
|
||||||
await int.reply({ content: `Role **${role.name}** added`, ephemeral: true });
|
await member.roles.add(role, `Button Roles on message ${int.message.id}`);
|
||||||
|
await int.reply({ content: `Role **${role.name}** added`, ephemeral: true });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await int.reply({
|
||||||
|
content: "A configuration error was encountered, please contact the Administrators!",
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
pluginData
|
||||||
|
.getPlugin(LogsPlugin)
|
||||||
|
.log(
|
||||||
|
LogType.BOT_ALERT,
|
||||||
|
`**A configuration error occured** on buttons for message ${int.message.id}, error: ${e}. We might be missing permissions!`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { MessageActionRow, MessageButton } from "discord.js";
|
||||||
|
import { TButtonOpts } from "../types";
|
||||||
|
|
||||||
|
export function splitButtonsIntoRows(actualButtons: MessageButton[], configButtons: TButtonOpts[]): MessageActionRow[] {
|
||||||
|
const rows: MessageActionRow[] = [];
|
||||||
|
let curRow = new MessageActionRow();
|
||||||
|
let consecutive = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < actualButtons.length; i++) {
|
||||||
|
const aBtn = actualButtons[i];
|
||||||
|
const cBtn = configButtons[i];
|
||||||
|
|
||||||
|
curRow.addComponents(aBtn);
|
||||||
|
if (((consecutive + 1) % 5 === 0 || cBtn.end_row) && i + 1 < actualButtons.length) {
|
||||||
|
rows.push(curRow);
|
||||||
|
curRow = new MessageActionRow();
|
||||||
|
consecutive = 0;
|
||||||
|
} else {
|
||||||
|
consecutive++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRowCount(configButtons: TButtonOpts[]): number {
|
||||||
|
let count = 1;
|
||||||
|
let consecutive = 0;
|
||||||
|
for (let i = 0; i < configButtons.length; i++) {
|
||||||
|
const cBtn = configButtons[i];
|
||||||
|
|
||||||
|
if (((consecutive + 1) % 5 === 0 || cBtn.end_row) && i + 1 < configButtons.length) {
|
||||||
|
count++;
|
||||||
|
consecutive = 0;
|
||||||
|
} else {
|
||||||
|
consecutive++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue