Make message with default buttons stateful
This commit is contained in:
parent
5efdf5ce95
commit
5c7c3c8cba
10 changed files with 158 additions and 47 deletions
|
@ -38,9 +38,17 @@ export class GuildButtonRoles extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async add(messageId: string, buttonId: string, buttonGroup: string, buttonName: string) {
|
||||
async getForButtonGroup(buttonGroup: string) {
|
||||
return this.buttonRoles.find({
|
||||
guild_id: this.guildId,
|
||||
button_group: buttonGroup,
|
||||
});
|
||||
}
|
||||
|
||||
async add(channelId: string, messageId: string, buttonId: string, buttonGroup: string, buttonName: string) {
|
||||
await this.buttonRoles.insert({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
message_id: messageId,
|
||||
button_id: buttonId,
|
||||
button_group: buttonGroup,
|
||||
|
|
|
@ -6,6 +6,10 @@ export class ButtonRole {
|
|||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
channel_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
message_id: string;
|
||||
|
|
|
@ -11,6 +11,11 @@ export class CreateButtonRolesTable1623018101018 implements MigrationInterface {
|
|||
type: "bigint",
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: "channel_id",
|
||||
type: "bigint",
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: "message_id",
|
||||
type: "bigint",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { PluginOptions } from "knub";
|
||||
import { GuildButtonRoles } from "src/data/GuildButtonRoles";
|
||||
import { GuildReactionRoles } from "../../data/GuildReactionRoles";
|
||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||
import { Queue } from "../../Queue";
|
||||
|
@ -10,6 +11,7 @@ import { PostButtonRolesCmd } from "./commands/PostButtonRolesCmd";
|
|||
import { RefreshReactionRolesCmd } from "./commands/RefreshReactionRolesCmd";
|
||||
import { AddReactionRoleEvt } from "./events/AddReactionRoleEvt";
|
||||
import { ButtonInteractionEvt } from "./events/ButtonInteractionEvt";
|
||||
import { MessageDeletedEvt } from "./events/MessageDeletedEvt";
|
||||
import { ConfigSchema, ReactionRolesPluginType } from "./types";
|
||||
import { autoRefreshLoop } from "./util/autoRefreshLoop";
|
||||
|
||||
|
@ -57,6 +59,7 @@ export const ReactionRolesPlugin = zeppelinGuildPlugin<ReactionRolesPluginType>(
|
|||
events: [
|
||||
AddReactionRoleEvt,
|
||||
ButtonInteractionEvt,
|
||||
MessageDeletedEvt,
|
||||
],
|
||||
|
||||
beforeLoad(pluginData) {
|
||||
|
@ -64,6 +67,7 @@ export const ReactionRolesPlugin = zeppelinGuildPlugin<ReactionRolesPluginType>(
|
|||
|
||||
state.reactionRoles = GuildReactionRoles.getGuildInstance(guild.id);
|
||||
state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id);
|
||||
state.buttonRoles = GuildButtonRoles.getGuildInstance(guild.id);
|
||||
state.reactionRemoveQueue = new Queue();
|
||||
state.roleChangeQueue = new Queue();
|
||||
state.pendingRoleChanges = new Map();
|
||||
|
|
|
@ -2,42 +2,33 @@ import { MessageActionRow, MessageButton, TextChannel } from "discord.js";
|
|||
import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { reactionRolesCmd } from "../types";
|
||||
import { ButtonMenuActions } from "../util/buttonMenuActions";
|
||||
import { createHash } from "crypto";
|
||||
import moment from "moment";
|
||||
|
||||
export const PostButtonRolesCmd = reactionRolesCmd({
|
||||
trigger: "reaction_roles post",
|
||||
permission: "can_manage",
|
||||
|
||||
signature: {
|
||||
button_group: ct.string(),
|
||||
channel: ct.textChannel(),
|
||||
buttonGroup: ct.string(),
|
||||
},
|
||||
|
||||
async run({ message: msg, args, pluginData }) {
|
||||
const cfg = pluginData.config.get();
|
||||
const group = cfg.button_groups[args.button_group];
|
||||
const group = cfg.button_groups[args.buttonGroup];
|
||||
|
||||
if (!group) {
|
||||
sendErrorMessage(pluginData, msg.channel, `No button group matches the name **${args.button_group}**`);
|
||||
}
|
||||
|
||||
const channel = pluginData.guild.channels.resolve(group.channel_id);
|
||||
if (!channel) {
|
||||
await sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`The ID ${group.channel_id} does not match a channel on the server`,
|
||||
);
|
||||
sendErrorMessage(pluginData, msg.channel, `No button group matches the name **${args.buttonGroup}**`);
|
||||
return;
|
||||
}
|
||||
|
||||
const buttons: MessageButton[] = [];
|
||||
for (const button of Object.values(group.default_buttons)) {
|
||||
let customId = "";
|
||||
if ((await pluginData.guild.roles.fetch(button.role_or_menu)) != null) {
|
||||
customId = `${args.button_group}::${ButtonMenuActions.GRANT_ROLE}::${button.role_or_menu}`;
|
||||
} else {
|
||||
customId = `${args.button_group}::${ButtonMenuActions.OPEN_MENU}::${button.role_or_menu}`;
|
||||
}
|
||||
const toInsert: Array<{ customId; buttonGroup; buttonName }> = [];
|
||||
for (const [buttonName, button] of Object.entries(group.default_buttons)) {
|
||||
const customId = createHash("md5")
|
||||
.update(`${buttonName}${moment.utc().valueOf()}`)
|
||||
.digest("hex");
|
||||
|
||||
const btn = new MessageButton()
|
||||
.setLabel(button.label)
|
||||
|
@ -51,15 +42,27 @@ export const PostButtonRolesCmd = reactionRolesCmd({
|
|||
}
|
||||
|
||||
buttons.push(btn);
|
||||
toInsert.push({ customId, buttonGroup: args.buttonGroup, buttonName });
|
||||
}
|
||||
const row = new MessageActionRow().addComponents(buttons);
|
||||
|
||||
try {
|
||||
await (channel as TextChannel).send({ content: group.message, components: [row], split: false });
|
||||
const newMsg = await args.channel.send({ content: group.message, components: [row], split: false });
|
||||
|
||||
for (const btn of toInsert) {
|
||||
await pluginData.state.buttonRoles.add(
|
||||
args.channel.id,
|
||||
newMsg.id,
|
||||
btn.customId,
|
||||
btn.buttonGroup,
|
||||
btn.buttonName,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Error trying to post message: ${e}`);
|
||||
return;
|
||||
}
|
||||
await sendSuccessMessage(pluginData, msg.channel, `Successfully posted message in <#${channel.id}>`);
|
||||
|
||||
await sendSuccessMessage(pluginData, msg.channel, `Successfully posted message in <#${args.channel.id}>`);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
import { MessageActionRow, MessageButton, MessageComponentInteraction } from "discord.js";
|
||||
import moment from "moment";
|
||||
import { LogType } from "src/data/LogType";
|
||||
import { logger } from "src/logger";
|
||||
import { LogsPlugin } from "src/plugins/Logs/LogsPlugin";
|
||||
import { MINUTES } from "src/utils";
|
||||
import { idToTimestamp } from "src/utils/idToTimestamp";
|
||||
import { reactionRolesEvt } from "../types";
|
||||
import {
|
||||
generateStatelessCustomId,
|
||||
resolveStatefulCustomId,
|
||||
BUTTON_CONTEXT_SEPARATOR,
|
||||
} from "../util/buttonCustomIdFunctions";
|
||||
import { ButtonMenuActions } from "../util/buttonMenuActions";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
|
||||
const BUTTON_INVALIDATION_TIME = 15 * MINUTES;
|
||||
|
||||
export const ButtonInteractionEvt = reactionRolesEvt({
|
||||
event: "interaction",
|
||||
|
@ -14,42 +25,60 @@ export const ButtonInteractionEvt = reactionRolesEvt({
|
|||
: null;
|
||||
if (!int) return;
|
||||
const cfg = meta.pluginData.config.get();
|
||||
const split = int.customID.split("::");
|
||||
const [groupName, action, roleOrMenu] = [split[0], split[1], split[2]];
|
||||
const split = int.customID.split(BUTTON_CONTEXT_SEPARATOR);
|
||||
const context = (await resolveStatefulCustomId(meta.pluginData, int.customID)) ?? {
|
||||
groupName: split[0],
|
||||
action: split[1],
|
||||
roleOrMenu: split[2],
|
||||
stateless: true,
|
||||
};
|
||||
|
||||
const group = cfg.button_groups[groupName];
|
||||
if (context.stateless) {
|
||||
const timeSinceCreation = moment.utc().valueOf() - idToTimestamp(int.message.id)!;
|
||||
if (timeSinceCreation >= BUTTON_INVALIDATION_TIME) {
|
||||
sendEphemeralReply(
|
||||
int,
|
||||
`Sorry, but these buttons are invalid because they are older than ${humanizeDuration(
|
||||
BUTTON_INVALIDATION_TIME,
|
||||
)}.\nIf the menu is still available, open it again to assign yourself roles!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const group = cfg.button_groups[context.groupName];
|
||||
if (!group) {
|
||||
await sendEphemeralReply(int, `A configuration error was encountered, please contact the Administrators!`);
|
||||
meta.pluginData
|
||||
.getPlugin(LogsPlugin)
|
||||
.log(
|
||||
LogType.BOT_ALERT,
|
||||
`**A configuration error occured** on buttons for message ${int.message.id}, group **${groupName}** not found in config`,
|
||||
`**A configuration error occured** on buttons for message ${int.message.id}, group **${context.groupName}** not found in config`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify that detected action is known by us
|
||||
if (!(<any>Object).values(ButtonMenuActions).includes(action)) {
|
||||
if (!(<any>Object).values(ButtonMenuActions).includes(context.action)) {
|
||||
await sendEphemeralReply(int, `A internal error was encountered, please contact the Administrators!`);
|
||||
meta.pluginData
|
||||
.getPlugin(LogsPlugin)
|
||||
.log(
|
||||
LogType.BOT_ALERT,
|
||||
`**A internal error occured** on buttons for message ${int.message.id}, action **${action}** is not known`,
|
||||
`**A internal error occured** on buttons for message ${int.message.id}, action **${context.action}** is not known`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === ButtonMenuActions.GRANT_ROLE) {
|
||||
const role = await meta.pluginData.guild.roles.fetch(roleOrMenu);
|
||||
if (context.action === ButtonMenuActions.GRANT_ROLE) {
|
||||
const role = await meta.pluginData.guild.roles.fetch(context.roleOrMenu);
|
||||
if (!role) {
|
||||
await sendEphemeralReply(int, `A configuration error was encountered, please contact the Administrators!`);
|
||||
meta.pluginData
|
||||
.getPlugin(LogsPlugin)
|
||||
.log(
|
||||
LogType.BOT_ALERT,
|
||||
`**A configuration error occured** on buttons for message ${int.message.id}, group **${groupName}** not found in config`,
|
||||
`**A configuration error occured** on buttons for message ${int.message.id}, group **${context.groupName}** not found in config`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -66,11 +95,10 @@ export const ButtonInteractionEvt = reactionRolesEvt({
|
|||
return;
|
||||
}
|
||||
|
||||
if (action === ButtonMenuActions.OPEN_MENU) {
|
||||
if (context.action === ButtonMenuActions.OPEN_MENU) {
|
||||
const menuButtons: MessageButton[] = [];
|
||||
for (const menuButton of Object.values(group.button_menus[roleOrMenu])) {
|
||||
let customId = "";
|
||||
customId = `${groupName}::${ButtonMenuActions.GRANT_ROLE}::${menuButton.role}`;
|
||||
for (const menuButton of Object.values(group.button_menus[context.roleOrMenu])) {
|
||||
const customId = await generateStatelessCustomId(meta.pluginData, context.groupName, menuButton.role_or_menu);
|
||||
|
||||
const btn = new MessageButton()
|
||||
.setLabel(menuButton.label)
|
||||
|
@ -91,7 +119,7 @@ export const ButtonInteractionEvt = reactionRolesEvt({
|
|||
.getPlugin(LogsPlugin)
|
||||
.log(
|
||||
LogType.BOT_ALERT,
|
||||
`**A configuration error occured** on buttons for message ${int.message.id}, menu **${roleOrMenu}** not found in config`,
|
||||
`**A configuration error occured** on buttons for message ${int.message.id}, menu **${context.roleOrMenu}** not found in config`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -102,7 +130,7 @@ export const ButtonInteractionEvt = reactionRolesEvt({
|
|||
}
|
||||
|
||||
logger.warn(
|
||||
`Action ${action} on button ${int.customID} (Guild: ${int.guildID}, Channel: ${int.channelID}) is unknown!`,
|
||||
`Action ${context.action} on button ${int.customID} (Guild: ${int.guildID}, Channel: ${int.channelID}) is unknown!`,
|
||||
);
|
||||
await sendEphemeralReply(int, `A internal error was encountered, please contact the Administrators!`);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { reactionRolesEvt } from "../types";
|
||||
|
||||
export const MessageDeletedEvt = reactionRolesEvt({
|
||||
event: "messageDelete",
|
||||
allowBots: true,
|
||||
allowSelf: true,
|
||||
|
||||
async listener(meta) {
|
||||
const pluginData = meta.pluginData;
|
||||
|
||||
await pluginData.state.buttonRoles.removeAllForMessageId(meta.args.message.id);
|
||||
await pluginData.state.reactionRoles.removeFromMessage(meta.args.message.id);
|
||||
},
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
import * as t from "io-ts";
|
||||
import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub";
|
||||
import { GuildButtonRoles } from "src/data/GuildButtonRoles";
|
||||
import { GuildReactionRoles } from "../../data/GuildReactionRoles";
|
||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||
import { Queue } from "../../Queue";
|
||||
|
@ -11,18 +12,10 @@ const ButtonOpts = t.type({
|
|||
});
|
||||
export type TButtonOpts = t.TypeOf<typeof ButtonOpts>;
|
||||
|
||||
const MenuButtonOpts = t.type({
|
||||
label: t.string,
|
||||
emoji: t.string,
|
||||
role: t.string,
|
||||
});
|
||||
export type TMenuButtonOpts = t.TypeOf<typeof MenuButtonOpts>;
|
||||
|
||||
const ButtonPairOpts = t.type({
|
||||
channel_id: t.string,
|
||||
message: t.string,
|
||||
default_buttons: t.record(t.string, ButtonOpts),
|
||||
button_menus: t.record(t.string, t.record(t.string, MenuButtonOpts)),
|
||||
button_menus: t.record(t.string, t.record(t.string, ButtonOpts)),
|
||||
});
|
||||
export type TButtonPairOpts = t.TypeOf<typeof ButtonPairOpts>;
|
||||
|
||||
|
@ -54,6 +47,7 @@ export interface ReactionRolesPluginType extends BasePluginType {
|
|||
state: {
|
||||
reactionRoles: GuildReactionRoles;
|
||||
savedMessages: GuildSavedMessages;
|
||||
buttonRoles: GuildButtonRoles;
|
||||
|
||||
reactionRemoveQueue: Queue;
|
||||
roleChangeQueue: Queue;
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { ReactionRolesPluginType } from "../types";
|
||||
import { ButtonMenuActions } from "./buttonMenuActions";
|
||||
|
||||
export const BUTTON_CONTEXT_SEPARATOR = "::";
|
||||
|
||||
export async function getButtonAction(pluginData: GuildPluginData<ReactionRolesPluginType>, roleOrMenu: string) {
|
||||
if (await pluginData.guild.roles.fetch(roleOrMenu)) {
|
||||
return ButtonMenuActions.GRANT_ROLE;
|
||||
} else {
|
||||
return ButtonMenuActions.OPEN_MENU;
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateStatelessCustomId(
|
||||
pluginData: GuildPluginData<ReactionRolesPluginType>,
|
||||
groupName: string,
|
||||
roleOrMenu: string,
|
||||
) {
|
||||
let id = groupName + BUTTON_CONTEXT_SEPARATOR;
|
||||
|
||||
id += `${await getButtonAction(pluginData, roleOrMenu)}${BUTTON_CONTEXT_SEPARATOR}${roleOrMenu}`;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
export async function resolveStatefulCustomId(pluginData: GuildPluginData<ReactionRolesPluginType>, id: string) {
|
||||
const button = await pluginData.state.buttonRoles.getForButtonId(id);
|
||||
|
||||
if (button) {
|
||||
const group = pluginData.config.get().button_groups[button.button_group];
|
||||
const cfgButton = group.default_buttons[button.button_name];
|
||||
|
||||
return {
|
||||
groupName: button.button_group,
|
||||
action: await getButtonAction(pluginData, cfgButton.role_or_menu),
|
||||
roleOrMenu: cfgButton.role_or_menu,
|
||||
stateless: false,
|
||||
};
|
||||
}
|
||||
}
|
10
backend/src/utils/idToTimestamp.ts
Normal file
10
backend/src/utils/idToTimestamp.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import moment from "moment";
|
||||
|
||||
const EPOCH = 1420070400000;
|
||||
|
||||
export function idToTimestamp(id: string) {
|
||||
if (typeof id === "number") return null;
|
||||
return moment(+id / 4194304 + EPOCH)
|
||||
.utc()
|
||||
.valueOf();
|
||||
}
|
Loading…
Add table
Reference in a new issue