mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-16 14:11:50 +00:00
feat: Context menu mod menu command
This commit is contained in:
parent
5a4e50b19d
commit
771ed76f64
17 changed files with 818 additions and 204 deletions
|
@ -83,6 +83,21 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRecentByUserId(userId: string, count: number, skip = 0): Promise<Case[]> {
|
||||||
|
return this.cases.find({
|
||||||
|
relations: this.getRelations(),
|
||||||
|
where: {
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: count,
|
||||||
|
order: {
|
||||||
|
case_number: "DESC",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getTotalCasesByModId(modId: string): Promise<number> {
|
async getTotalCasesByModId(modId: string): Promise<number> {
|
||||||
return this.cases.count({
|
return this.cases.count({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -1,32 +1,30 @@
|
||||||
import { PluginOptions } from "knub";
|
import { PluginOptions } from "knub";
|
||||||
import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks";
|
import { GuildCases } from "../../data/GuildCases";
|
||||||
import { makeIoTsConfigParser } from "../../pluginUtils";
|
import { makeIoTsConfigParser } from "../../pluginUtils";
|
||||||
|
import { trimPluginDescription } from "../../utils";
|
||||||
|
import { CasesPlugin } from "../Cases/CasesPlugin";
|
||||||
import { LogsPlugin } from "../Logs/LogsPlugin";
|
import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||||
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
||||||
import { UtilityPlugin } from "../Utility/UtilityPlugin";
|
import { UtilityPlugin } from "../Utility/UtilityPlugin";
|
||||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||||
import { ContextClickedEvt } from "./events/ContextClickedEvt";
|
import { ModMenuCmd } from "./commands/ModMenuCmd";
|
||||||
import { ConfigSchema, ContextMenuPluginType } from "./types";
|
import { ConfigSchema, ContextMenuPluginType } from "./types";
|
||||||
import { loadAllCommands } from "./utils/loadAllCommands";
|
|
||||||
|
|
||||||
const defaultOptions: PluginOptions<ContextMenuPluginType> = {
|
const defaultOptions: PluginOptions<ContextMenuPluginType> = {
|
||||||
config: {
|
config: {
|
||||||
can_use: false,
|
can_use: false,
|
||||||
|
|
||||||
user_muteindef: false,
|
can_open_mod_menu: false,
|
||||||
user_mute1d: false,
|
|
||||||
user_mute1h: false,
|
|
||||||
user_info: false,
|
|
||||||
|
|
||||||
message_clean10: false,
|
log_channel: null,
|
||||||
message_clean25: false,
|
|
||||||
message_clean50: false,
|
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
level: ">=50",
|
level: ">=50",
|
||||||
config: {
|
config: {
|
||||||
can_use: true,
|
can_use: true,
|
||||||
|
|
||||||
|
can_open_mod_menu: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -34,24 +32,24 @@ const defaultOptions: PluginOptions<ContextMenuPluginType> = {
|
||||||
|
|
||||||
export const ContextMenuPlugin = zeppelinGuildPlugin<ContextMenuPluginType>()({
|
export const ContextMenuPlugin = zeppelinGuildPlugin<ContextMenuPluginType>()({
|
||||||
name: "context_menu",
|
name: "context_menu",
|
||||||
showInDocs: false,
|
showInDocs: true,
|
||||||
|
info: {
|
||||||
|
prettyName: "Context Menus",
|
||||||
|
description: trimPluginDescription(`
|
||||||
|
This plugin provides command shortcuts via context menus
|
||||||
|
`),
|
||||||
|
configSchema: ConfigSchema,
|
||||||
|
},
|
||||||
|
|
||||||
dependencies: () => [MutesPlugin, LogsPlugin, UtilityPlugin],
|
dependencies: () => [CasesPlugin, MutesPlugin, LogsPlugin, UtilityPlugin],
|
||||||
configParser: makeIoTsConfigParser(ConfigSchema),
|
configParser: makeIoTsConfigParser(ConfigSchema),
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
|
|
||||||
// prettier-ignore
|
contextMenuCommands: [ModMenuCmd],
|
||||||
events: [
|
|
||||||
ContextClickedEvt,
|
|
||||||
],
|
|
||||||
|
|
||||||
beforeLoad(pluginData) {
|
beforeLoad(pluginData) {
|
||||||
const { state, guild } = pluginData;
|
const { state, guild } = pluginData;
|
||||||
|
|
||||||
state.contextMenuLinks = new GuildContextMenuLinks(guild.id);
|
state.cases = GuildCases.getGuildInstance(guild.id);
|
||||||
},
|
|
||||||
|
|
||||||
afterLoad(pluginData) {
|
|
||||||
loadAllCommands(pluginData);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
97
backend/src/plugins/ContextMenus/actions/ban.ts
Normal file
97
backend/src/plugins/ContextMenus/actions/ban.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonInteraction,
|
||||||
|
ModalBuilder,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
} from "discord.js";
|
||||||
|
import humanizeDuration from "humanize-duration";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { canActOn } from "src/pluginUtils";
|
||||||
|
import { ModActionsPlugin } from "src/plugins/ModActions/ModActionsPlugin";
|
||||||
|
import { convertDelayStringToMS, renderUserUsername } from "../../../utils";
|
||||||
|
import { CaseArgs } from "../../Cases/types";
|
||||||
|
import { MODAL_TIMEOUT } from "../commands/ModMenuCmd";
|
||||||
|
import { ContextMenuPluginType } from "../types";
|
||||||
|
|
||||||
|
async function banAction(
|
||||||
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
|
duration: string | undefined,
|
||||||
|
reason: string | undefined,
|
||||||
|
target: string,
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
) {
|
||||||
|
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
|
||||||
|
const userCfg = await pluginData.config.getMatchingConfig({
|
||||||
|
channelId: interaction.channelId,
|
||||||
|
member: executingMember,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modactions = pluginData.getPlugin(ModActionsPlugin);
|
||||||
|
if (!userCfg.can_use || !(await modactions.hasBanPermission(executingMember, interaction.channelId))) {
|
||||||
|
await interaction.editReply({ content: "Cannot ban: insufficient permissions", embeds: [], components: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMember = await pluginData.guild.members.fetch(target);
|
||||||
|
if (!canActOn(pluginData, executingMember, targetMember)) {
|
||||||
|
await interaction.editReply({ content: "Cannot ban: insufficient permissions", embeds: [], components: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const caseArgs: Partial<CaseArgs> = {
|
||||||
|
modId: executingMember.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const durationMs = duration ? convertDelayStringToMS(duration)! : undefined;
|
||||||
|
const result = await modactions.banUserId(target, reason, { caseArgs }, durationMs);
|
||||||
|
if (result.status === "failed") {
|
||||||
|
await interaction.editReply({ content: "ERROR: Failed to ban user", embeds: [], components: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userName = renderUserUsername(targetMember.user);
|
||||||
|
const messageResultText = result.notifyResult.text ? ` (${result.notifyResult.text})` : "";
|
||||||
|
const banMessage = `Banned **${userName}** ${
|
||||||
|
durationMs ? `for ${humanizeDuration(durationMs)}` : "indefinitely"
|
||||||
|
} (Case #${result.case.case_number})${messageResultText}`;
|
||||||
|
|
||||||
|
await interaction.editReply({ content: banMessage, embeds: [], components: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function launchBanActionModal(
|
||||||
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
target: string,
|
||||||
|
) {
|
||||||
|
const modal = new ModalBuilder().setCustomId("ban").setTitle("Ban");
|
||||||
|
|
||||||
|
const durationIn = new TextInputBuilder()
|
||||||
|
.setCustomId("duration")
|
||||||
|
.setLabel("Duration (Optional)")
|
||||||
|
.setRequired(false)
|
||||||
|
.setStyle(TextInputStyle.Short);
|
||||||
|
|
||||||
|
const reasonIn = new TextInputBuilder()
|
||||||
|
.setCustomId("reason")
|
||||||
|
.setLabel("Reason (Optional)")
|
||||||
|
.setRequired(false)
|
||||||
|
.setStyle(TextInputStyle.Paragraph);
|
||||||
|
|
||||||
|
const durationRow = new ActionRowBuilder<TextInputBuilder>().addComponents(durationIn);
|
||||||
|
const reasonRow = new ActionRowBuilder<TextInputBuilder>().addComponents(reasonIn);
|
||||||
|
|
||||||
|
modal.addComponents(durationRow, reasonRow);
|
||||||
|
|
||||||
|
await interaction.showModal(modal);
|
||||||
|
const submitted: ModalSubmitInteraction = await interaction.awaitModalSubmit({ time: MODAL_TIMEOUT });
|
||||||
|
if (submitted) {
|
||||||
|
await submitted.deferUpdate();
|
||||||
|
|
||||||
|
const duration = submitted.fields.getTextInputValue("duration");
|
||||||
|
const reason = submitted.fields.getTextInputValue("reason");
|
||||||
|
|
||||||
|
await banAction(pluginData, duration, reason, target, interaction);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,22 @@
|
||||||
import { ContextMenuCommandInteraction, TextChannel } from "discord.js";
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonInteraction,
|
||||||
|
ModalBuilder,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
} from "discord.js";
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
|
|
||||||
import { UtilityPlugin } from "../../../plugins/Utility/UtilityPlugin";
|
import { UtilityPlugin } from "../../../plugins/Utility/UtilityPlugin";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { MODAL_TIMEOUT } from "../commands/ModMenuCmd";
|
||||||
import { ContextMenuPluginType } from "../types";
|
import { ContextMenuPluginType } from "../types";
|
||||||
|
|
||||||
export async function cleanAction(
|
export async function cleanAction(
|
||||||
pluginData: GuildPluginData<ContextMenuPluginType>,
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
amount: number,
|
amount: number,
|
||||||
interaction: ContextMenuCommandInteraction,
|
target: string,
|
||||||
|
interaction: ButtonInteraction,
|
||||||
) {
|
) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
|
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
|
||||||
const userCfg = await pluginData.config.getMatchingConfig({
|
const userCfg = await pluginData.config.getMatchingConfig({
|
||||||
channelId: interaction.channelId,
|
channelId: interaction.channelId,
|
||||||
|
@ -19,32 +25,42 @@ export async function cleanAction(
|
||||||
const utility = pluginData.getPlugin(UtilityPlugin);
|
const utility = pluginData.getPlugin(UtilityPlugin);
|
||||||
|
|
||||||
if (!userCfg.can_use || !(await utility.hasPermission(executingMember, interaction.channelId, "can_clean"))) {
|
if (!userCfg.can_use || !(await utility.hasPermission(executingMember, interaction.channelId, "can_clean"))) {
|
||||||
await interaction.followUp({ content: "Cannot clean: insufficient permissions" });
|
await interaction.editReply({ content: "Cannot clean: insufficient permissions", embeds: [], components: [] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetMessage = interaction.channel
|
// TODO: Implement message cleaning
|
||||||
? await interaction.channel.messages.fetch(interaction.targetId)
|
await interaction.editReply({
|
||||||
: await (pluginData.guild.channels.resolve(interaction.channelId) as TextChannel).messages.fetch(
|
content: `TODO: Implementation incomplete`,
|
||||||
interaction.targetId,
|
embeds: [],
|
||||||
);
|
components: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const targetUserOnly = false;
|
export async function launchCleanActionModal(
|
||||||
const deletePins = false;
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
const user = undefined;
|
interaction: ButtonInteraction,
|
||||||
|
target: string,
|
||||||
|
) {
|
||||||
|
const modal = new ModalBuilder().setCustomId("clean").setTitle("Clean");
|
||||||
|
|
||||||
try {
|
const amountIn = new TextInputBuilder().setCustomId("amount").setLabel("Amount").setStyle(TextInputStyle.Short);
|
||||||
await interaction.followUp(`Cleaning... Amount: ${amount}, User Only: ${targetUserOnly}, Pins: ${deletePins}`);
|
|
||||||
utility.clean({ count: amount, user, channel: targetMessage.channel.id, "delete-pins": deletePins }, targetMessage);
|
|
||||||
} catch (e) {
|
|
||||||
await interaction.followUp({ ephemeral: true, content: "Plugin error, please check your BOT_ALERTs" });
|
|
||||||
|
|
||||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
const amountRow = new ActionRowBuilder<TextInputBuilder>().addComponents(amountIn);
|
||||||
pluginData.getPlugin(LogsPlugin).logBotAlert({
|
|
||||||
body: `Failed to clean in <#${interaction.channelId}> in ContextMenu action \`clean\`:_ ${e}`,
|
modal.addComponents(amountRow);
|
||||||
});
|
|
||||||
} else {
|
await interaction.showModal(modal);
|
||||||
throw e;
|
const submitted: ModalSubmitInteraction = await interaction.awaitModalSubmit({ time: MODAL_TIMEOUT });
|
||||||
|
if (submitted) {
|
||||||
|
await submitted.deferUpdate();
|
||||||
|
|
||||||
|
const amount = submitted.fields.getTextInputValue("amount");
|
||||||
|
if (isNaN(Number(amount))) {
|
||||||
|
interaction.editReply({ content: `ERROR: Amount ${amount} is invalid`, embeds: [], components: [] });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await cleanAction(pluginData, Number(amount), target, interaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { ContextMenuCommandInteraction } from "discord.js";
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonInteraction,
|
||||||
|
ModalBuilder,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
} from "discord.js";
|
||||||
import humanizeDuration from "humanize-duration";
|
import humanizeDuration from "humanize-duration";
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import { canActOn } from "src/pluginUtils";
|
import { canActOn } from "src/pluginUtils";
|
||||||
|
@ -8,14 +15,16 @@ import { convertDelayStringToMS } from "../../../utils";
|
||||||
import { CaseArgs } from "../../Cases/types";
|
import { CaseArgs } from "../../Cases/types";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
import { MutesPlugin } from "../../Mutes/MutesPlugin";
|
import { MutesPlugin } from "../../Mutes/MutesPlugin";
|
||||||
|
import { MODAL_TIMEOUT } from "../commands/ModMenuCmd";
|
||||||
import { ContextMenuPluginType } from "../types";
|
import { ContextMenuPluginType } from "../types";
|
||||||
|
|
||||||
export async function muteAction(
|
async function muteAction(
|
||||||
pluginData: GuildPluginData<ContextMenuPluginType>,
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
duration: string | undefined,
|
duration: string | undefined,
|
||||||
interaction: ContextMenuCommandInteraction,
|
reason: string | undefined,
|
||||||
|
target: string,
|
||||||
|
interaction: ButtonInteraction,
|
||||||
) {
|
) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
|
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
|
||||||
const userCfg = await pluginData.config.getMatchingConfig({
|
const userCfg = await pluginData.config.getMatchingConfig({
|
||||||
channelId: interaction.channelId,
|
channelId: interaction.channelId,
|
||||||
|
@ -24,43 +33,76 @@ export async function muteAction(
|
||||||
|
|
||||||
const modactions = pluginData.getPlugin(ModActionsPlugin);
|
const modactions = pluginData.getPlugin(ModActionsPlugin);
|
||||||
if (!userCfg.can_use || !(await modactions.hasMutePermission(executingMember, interaction.channelId))) {
|
if (!userCfg.can_use || !(await modactions.hasMutePermission(executingMember, interaction.channelId))) {
|
||||||
await interaction.followUp({ content: "Cannot mute: insufficient permissions" });
|
await interaction.editReply({ content: "Cannot mute: insufficient permissions", embeds: [], components: [] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const durationMs = duration ? convertDelayStringToMS(duration)! : undefined;
|
const targetMember = await pluginData.guild.members.fetch(target);
|
||||||
const mutes = pluginData.getPlugin(MutesPlugin);
|
|
||||||
const userId = interaction.targetId;
|
|
||||||
const targetMember = await pluginData.guild.members.fetch(interaction.targetId);
|
|
||||||
|
|
||||||
if (!canActOn(pluginData, executingMember, targetMember)) {
|
if (!canActOn(pluginData, executingMember, targetMember)) {
|
||||||
await interaction.followUp({ ephemeral: true, content: "Cannot mute: insufficient permissions" });
|
await interaction.editReply({ content: "Cannot mute: insufficient permissions", embeds: [], components: [] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const caseArgs: Partial<CaseArgs> = {
|
const caseArgs: Partial<CaseArgs> = {
|
||||||
modId: executingMember.id,
|
modId: executingMember.id,
|
||||||
};
|
};
|
||||||
|
const mutes = pluginData.getPlugin(MutesPlugin);
|
||||||
|
const durationMs = duration ? convertDelayStringToMS(duration)! : undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await mutes.muteUser(userId, durationMs, "Context Menu Action", { caseArgs });
|
const result = await mutes.muteUser(target, durationMs, reason, { caseArgs });
|
||||||
|
|
||||||
|
const messageResultText = result.notifyResult.text ? ` (${result.notifyResult.text})` : "";
|
||||||
const muteMessage = `Muted **${result.case.user_name}** ${
|
const muteMessage = `Muted **${result.case.user_name}** ${
|
||||||
durationMs ? `for ${humanizeDuration(durationMs)}` : "indefinitely"
|
durationMs ? `for ${humanizeDuration(durationMs)}` : "indefinitely"
|
||||||
} (Case #${result.case.case_number}) (user notified via ${
|
} (Case #${result.case.case_number})${messageResultText}`;
|
||||||
result.notifyResult.method ?? "dm"
|
|
||||||
})\nPlease update the new case with the \`update\` command`;
|
|
||||||
|
|
||||||
await interaction.followUp({ ephemeral: true, content: muteMessage });
|
await interaction.editReply({ content: muteMessage, embeds: [], components: [] });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await interaction.followUp({ ephemeral: true, content: "Plugin error, please check your BOT_ALERTs" });
|
await interaction.editReply({ content: "Plugin error, please check your BOT_ALERTs", embeds: [], components: [] });
|
||||||
|
|
||||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
||||||
pluginData.getPlugin(LogsPlugin).logBotAlert({
|
pluginData.getPlugin(LogsPlugin).logBotAlert({
|
||||||
body: `Failed to mute <@!${userId}> in ContextMenu action \`mute\` because a mute role has not been specified in server config`,
|
body: `Failed to mute <@!${target}> in ContextMenu action \`mute\` because a mute role has not been specified in server config`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function launchMuteActionModal(
|
||||||
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
target: string,
|
||||||
|
) {
|
||||||
|
const modal = new ModalBuilder().setCustomId("mute").setTitle("Mute");
|
||||||
|
|
||||||
|
const durationIn = new TextInputBuilder()
|
||||||
|
.setCustomId("duration")
|
||||||
|
.setLabel("Duration (Optional)")
|
||||||
|
.setRequired(false)
|
||||||
|
.setStyle(TextInputStyle.Short);
|
||||||
|
|
||||||
|
const reasonIn = new TextInputBuilder()
|
||||||
|
.setCustomId("reason")
|
||||||
|
.setLabel("Reason (Optional)")
|
||||||
|
.setRequired(false)
|
||||||
|
.setStyle(TextInputStyle.Paragraph);
|
||||||
|
|
||||||
|
const durationRow = new ActionRowBuilder<TextInputBuilder>().addComponents(durationIn);
|
||||||
|
const reasonRow = new ActionRowBuilder<TextInputBuilder>().addComponents(reasonIn);
|
||||||
|
|
||||||
|
modal.addComponents(durationRow, reasonRow);
|
||||||
|
|
||||||
|
await interaction.showModal(modal);
|
||||||
|
const submitted: ModalSubmitInteraction = await interaction.awaitModalSubmit({ time: MODAL_TIMEOUT });
|
||||||
|
if (submitted) {
|
||||||
|
await submitted.deferUpdate();
|
||||||
|
|
||||||
|
const duration = submitted.fields.getTextInputValue("duration");
|
||||||
|
const reason = submitted.fields.getTextInputValue("reason");
|
||||||
|
|
||||||
|
await muteAction(pluginData, duration, reason, target, interaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
88
backend/src/plugins/ContextMenus/actions/note.ts
Normal file
88
backend/src/plugins/ContextMenus/actions/note.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonInteraction,
|
||||||
|
ModalBuilder,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
} from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { canActOn } from "src/pluginUtils";
|
||||||
|
import { ModActionsPlugin } from "src/plugins/ModActions/ModActionsPlugin";
|
||||||
|
import { CaseTypes } from "../../../data/CaseTypes";
|
||||||
|
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
||||||
|
import { renderUserUsername } from "../../../utils";
|
||||||
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
|
import { MODAL_TIMEOUT } from "../commands/ModMenuCmd";
|
||||||
|
import { ContextMenuPluginType } from "../types";
|
||||||
|
|
||||||
|
async function noteAction(
|
||||||
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
|
reason: string,
|
||||||
|
target: string,
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
) {
|
||||||
|
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
|
||||||
|
const userCfg = await pluginData.config.getMatchingConfig({
|
||||||
|
channelId: interaction.channelId,
|
||||||
|
member: executingMember,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modactions = pluginData.getPlugin(ModActionsPlugin);
|
||||||
|
if (!userCfg.can_use || !(await modactions.hasNotePermission(executingMember, interaction.channelId))) {
|
||||||
|
await interaction.editReply({ content: "Cannot note: insufficient permissions", embeds: [], components: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMember = await pluginData.guild.members.fetch(target);
|
||||||
|
if (!canActOn(pluginData, executingMember, targetMember)) {
|
||||||
|
await interaction.editReply({ content: "Cannot mute: insufficient permissions", embeds: [], components: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const createdCase = await casesPlugin.createCase({
|
||||||
|
userId: target,
|
||||||
|
modId: executingMember.id,
|
||||||
|
type: CaseTypes.Note,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMemberNote({
|
||||||
|
mod: interaction.user,
|
||||||
|
user: targetMember.user,
|
||||||
|
caseNumber: createdCase.case_number,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userName = renderUserUsername(targetMember.user);
|
||||||
|
await interaction.editReply({
|
||||||
|
content: `Note added on **${userName}** (Case #${createdCase.case_number})`,
|
||||||
|
embeds: [],
|
||||||
|
components: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function launchNoteActionModal(
|
||||||
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
target: string,
|
||||||
|
) {
|
||||||
|
const modal = new ModalBuilder().setCustomId("note").setTitle("Note");
|
||||||
|
|
||||||
|
const reasonIn = new TextInputBuilder().setCustomId("reason").setLabel("Note").setStyle(TextInputStyle.Paragraph);
|
||||||
|
|
||||||
|
const reasonRow = new ActionRowBuilder<TextInputBuilder>().addComponents(reasonIn);
|
||||||
|
|
||||||
|
modal.addComponents(reasonRow);
|
||||||
|
|
||||||
|
await interaction.showModal(modal);
|
||||||
|
const submitted: ModalSubmitInteraction = await interaction.awaitModalSubmit({ time: MODAL_TIMEOUT });
|
||||||
|
if (submitted) {
|
||||||
|
await submitted.deferUpdate();
|
||||||
|
|
||||||
|
const reason = submitted.fields.getTextInputValue("reason");
|
||||||
|
|
||||||
|
await noteAction(pluginData, reason, target, interaction);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
import { ContextMenuCommandInteraction } from "discord.js";
|
|
||||||
import { GuildPluginData } from "knub";
|
|
||||||
import { UtilityPlugin } from "../../../plugins/Utility/UtilityPlugin";
|
|
||||||
import { ContextMenuPluginType } from "../types";
|
|
||||||
|
|
||||||
export async function userInfoAction(
|
|
||||||
pluginData: GuildPluginData<ContextMenuPluginType>,
|
|
||||||
interaction: ContextMenuCommandInteraction,
|
|
||||||
) {
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
|
|
||||||
const userCfg = await pluginData.config.getMatchingConfig({
|
|
||||||
channelId: interaction.channelId,
|
|
||||||
member: executingMember,
|
|
||||||
});
|
|
||||||
const utility = pluginData.getPlugin(UtilityPlugin);
|
|
||||||
|
|
||||||
if (userCfg.can_use && (await utility.hasPermission(executingMember, interaction.channelId, "can_userinfo"))) {
|
|
||||||
const embed = await utility.userInfo(interaction.targetId, interaction.user.id);
|
|
||||||
if (!embed) {
|
|
||||||
await interaction.followUp({ content: "Cannot info: internal error" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await interaction.followUp({ embeds: [embed] });
|
|
||||||
} else {
|
|
||||||
await interaction.followUp({ content: "Cannot info: insufficient permissions" });
|
|
||||||
}
|
|
||||||
}
|
|
80
backend/src/plugins/ContextMenus/actions/warn.ts
Normal file
80
backend/src/plugins/ContextMenus/actions/warn.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonInteraction,
|
||||||
|
ModalBuilder,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
} from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { canActOn } from "src/pluginUtils";
|
||||||
|
import { ModActionsPlugin } from "src/plugins/ModActions/ModActionsPlugin";
|
||||||
|
import { renderUserUsername } from "../../../utils";
|
||||||
|
import { CaseArgs } from "../../Cases/types";
|
||||||
|
import { MODAL_TIMEOUT } from "../commands/ModMenuCmd";
|
||||||
|
import { ContextMenuPluginType } from "../types";
|
||||||
|
|
||||||
|
async function warnAction(
|
||||||
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
|
reason: string,
|
||||||
|
target: string,
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
) {
|
||||||
|
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
|
||||||
|
const userCfg = await pluginData.config.getMatchingConfig({
|
||||||
|
channelId: interaction.channelId,
|
||||||
|
member: executingMember,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modactions = pluginData.getPlugin(ModActionsPlugin);
|
||||||
|
if (!userCfg.can_use || !(await modactions.hasWarnPermission(executingMember, interaction.channelId))) {
|
||||||
|
await interaction.editReply({ content: "Cannot warn: insufficient permissions", embeds: [], components: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMember = await pluginData.guild.members.fetch(target);
|
||||||
|
if (!canActOn(pluginData, executingMember, targetMember)) {
|
||||||
|
await interaction.editReply({ content: "Cannot mute: insufficient permissions", embeds: [], components: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const caseArgs: Partial<CaseArgs> = {
|
||||||
|
modId: executingMember.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await modactions.warnMember(targetMember, reason, { caseArgs });
|
||||||
|
if (result.status === "failed") {
|
||||||
|
await interaction.editReply({ content: "Failed to warn user", embeds: [], components: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userName = renderUserUsername(targetMember.user);
|
||||||
|
const messageResultText = result.notifyResult.text ? ` (${result.notifyResult.text})` : "";
|
||||||
|
const muteMessage = `Warned **${userName}** (Case #${result.case.case_number})${messageResultText}`;
|
||||||
|
|
||||||
|
await interaction.editReply({ content: muteMessage, embeds: [], components: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function launchWarnActionModal(
|
||||||
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
target: string,
|
||||||
|
) {
|
||||||
|
const modal = new ModalBuilder().setCustomId("warn").setTitle("Warn");
|
||||||
|
|
||||||
|
const reasonIn = new TextInputBuilder().setCustomId("reason").setLabel("Reason").setStyle(TextInputStyle.Paragraph);
|
||||||
|
|
||||||
|
const reasonRow = new ActionRowBuilder<TextInputBuilder>().addComponents(reasonIn);
|
||||||
|
|
||||||
|
modal.addComponents(reasonRow);
|
||||||
|
|
||||||
|
await interaction.showModal(modal);
|
||||||
|
const submitted: ModalSubmitInteraction = await interaction.awaitModalSubmit({ time: MODAL_TIMEOUT });
|
||||||
|
if (submitted) {
|
||||||
|
await submitted.deferUpdate();
|
||||||
|
|
||||||
|
const reason = submitted.fields.getTextInputValue("reason");
|
||||||
|
|
||||||
|
await warnAction(pluginData, reason, target, interaction);
|
||||||
|
}
|
||||||
|
}
|
319
backend/src/plugins/ContextMenus/commands/ModMenuCmd.ts
Normal file
319
backend/src/plugins/ContextMenus/commands/ModMenuCmd.ts
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
import {
|
||||||
|
APIEmbed,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonInteraction,
|
||||||
|
ButtonStyle,
|
||||||
|
ContextMenuCommandInteraction,
|
||||||
|
User,
|
||||||
|
} from "discord.js";
|
||||||
|
import { GuildPluginData, guildPluginUserContextMenuCommand } from "knub";
|
||||||
|
import { Case } from "../../../data/entities/Case";
|
||||||
|
import { getUserInfoEmbed } from "../../../plugins/Utility/functions/getUserInfoEmbed";
|
||||||
|
import { SECONDS, UnknownUser, emptyEmbedValue, renderUserUsername, resolveUser, trimLines } from "../../../utils";
|
||||||
|
import { asyncMap } from "../../../utils/async";
|
||||||
|
import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields";
|
||||||
|
import { getGuildPrefix } from "../../../utils/getGuildPrefix";
|
||||||
|
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||||
|
import { UtilityPlugin } from "../../Utility/UtilityPlugin";
|
||||||
|
import { launchBanActionModal } from "../actions/ban";
|
||||||
|
import { launchCleanActionModal } from "../actions/clean";
|
||||||
|
import { launchMuteActionModal } from "../actions/mute";
|
||||||
|
import { launchNoteActionModal } from "../actions/note";
|
||||||
|
import { launchWarnActionModal } from "../actions/warn";
|
||||||
|
import {
|
||||||
|
ContextMenuPluginType,
|
||||||
|
LoadModMenuPageFn,
|
||||||
|
ModMenuActionOpts,
|
||||||
|
ModMenuActionType,
|
||||||
|
ModMenuNavigationType,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
export const MODAL_TIMEOUT = 60 * SECONDS;
|
||||||
|
const MOD_MENU_TIMEOUT = 60 * SECONDS;
|
||||||
|
const CASES_PER_PAGE = 10;
|
||||||
|
|
||||||
|
export const ModMenuCmd = guildPluginUserContextMenuCommand({
|
||||||
|
name: "Mod Menu",
|
||||||
|
async run({ pluginData, interaction }) {
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
|
// Run permission checks for executing user.
|
||||||
|
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
|
||||||
|
const userCfg = await pluginData.config.getMatchingConfig({
|
||||||
|
channelId: interaction.channelId,
|
||||||
|
member: executingMember,
|
||||||
|
});
|
||||||
|
const utility = pluginData.getPlugin(UtilityPlugin);
|
||||||
|
if (
|
||||||
|
!userCfg.can_use ||
|
||||||
|
(await !utility.hasPermission(executingMember, interaction.channelId, "can_open_mod_menu"))
|
||||||
|
) {
|
||||||
|
await interaction.followUp({ content: "Error: Insufficient Permissions" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await resolveUser(pluginData.client, interaction.targetId);
|
||||||
|
if (!user.id) {
|
||||||
|
await interaction.followUp("Error: User not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load cases and display mod menu
|
||||||
|
const cases: Case[] = await pluginData.state.cases.with("notes").getByUserId(user.id);
|
||||||
|
const userName =
|
||||||
|
user instanceof UnknownUser && cases.length ? cases[cases.length - 1].user_name : renderUserUsername(user);
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const totalCases = cases.length;
|
||||||
|
const totalPages: number = Math.max(Math.ceil(totalCases / CASES_PER_PAGE), 1);
|
||||||
|
const prefix = getGuildPrefix(pluginData);
|
||||||
|
const infoEmbed = await getUserInfoEmbed(pluginData, user.id, false);
|
||||||
|
displayModMenu(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
totalPages,
|
||||||
|
async (page) => {
|
||||||
|
const pageCases: Case[] = await pluginData.state.cases
|
||||||
|
.with("notes")
|
||||||
|
.getRecentByUserId(user.id, CASES_PER_PAGE, (page - 1) * CASES_PER_PAGE);
|
||||||
|
const lines = await asyncMap(pageCases, (c) => casesPlugin.getCaseSummary(c, true, interaction.targetId));
|
||||||
|
|
||||||
|
const firstCaseNum = (page - 1) * CASES_PER_PAGE + 1;
|
||||||
|
const lastCaseNum = Math.min(page * CASES_PER_PAGE, totalCases);
|
||||||
|
const title =
|
||||||
|
lines.length == 0
|
||||||
|
? `${userName}`
|
||||||
|
: `Most recent cases for ${userName} | ${firstCaseNum}-${lastCaseNum} of ${totalCases}`;
|
||||||
|
const embedFields =
|
||||||
|
lines.length == 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: `**No cases found**`,
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
...getChunkedEmbedFields(
|
||||||
|
emptyEmbedValue,
|
||||||
|
lines.length == 0 ? `No cases found for **${userName}**` : lines.join("\n"),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
name: emptyEmbedValue,
|
||||||
|
value: trimLines(`
|
||||||
|
Use \`${prefix}case <num>\` to see more information about an individual case
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const embed = {
|
||||||
|
author: {
|
||||||
|
name: title,
|
||||||
|
icon_url: user instanceof User ? user.displayAvatarURL() : undefined,
|
||||||
|
},
|
||||||
|
fields: embedFields,
|
||||||
|
footer: { text: `Page ${page}/${totalPages}` },
|
||||||
|
} satisfies APIEmbed;
|
||||||
|
|
||||||
|
return embed;
|
||||||
|
},
|
||||||
|
infoEmbed,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function displayModMenu(
|
||||||
|
pluginData: GuildPluginData<ContextMenuPluginType>,
|
||||||
|
interaction: ContextMenuCommandInteraction,
|
||||||
|
totalPages: number,
|
||||||
|
loadPage: LoadModMenuPageFn,
|
||||||
|
infoEmbed: APIEmbed | null,
|
||||||
|
) {
|
||||||
|
if (interaction.deferred == false) {
|
||||||
|
await interaction.deferReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstButton = new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
.setLabel("<<")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.FIRST }))
|
||||||
|
.setDisabled(true);
|
||||||
|
const prevButton = new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
.setLabel("<")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.PREV }))
|
||||||
|
.setDisabled(true);
|
||||||
|
const infoButton = new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
.setLabel("Info")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.INFO }))
|
||||||
|
.setDisabled(infoEmbed != null ? false : true);
|
||||||
|
const nextButton = new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
.setLabel(">")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.NEXT }))
|
||||||
|
.setDisabled(totalPages > 1 ? false : true);
|
||||||
|
const lastButton = new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
.setLabel(">>")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.LAST }))
|
||||||
|
.setDisabled(totalPages > 1 ? false : true);
|
||||||
|
const navigationButtons = [firstButton, prevButton, infoButton, nextButton, lastButton] satisfies ButtonBuilder[];
|
||||||
|
|
||||||
|
const moderationButtons = [
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
.setLabel("Note")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.NOTE, target: interaction.targetId })),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
.setLabel("Warn")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.WARN, target: interaction.targetId })),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
.setLabel("Clean")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.CLEAN, target: interaction.targetId })),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
.setLabel("Mute")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.MUTE, target: interaction.targetId })),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
.setLabel("Ban")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.BAN, target: interaction.targetId })),
|
||||||
|
] satisfies ButtonBuilder[];
|
||||||
|
|
||||||
|
const navigationRow = new ActionRowBuilder<ButtonBuilder>().addComponents(navigationButtons);
|
||||||
|
const moderationRow = new ActionRowBuilder<ButtonBuilder>().addComponents(moderationButtons);
|
||||||
|
|
||||||
|
let page = 1;
|
||||||
|
const currentPage = await interaction.editReply({
|
||||||
|
embeds: [await loadPage(page)],
|
||||||
|
components: [navigationRow, moderationRow],
|
||||||
|
});
|
||||||
|
|
||||||
|
const collector = await currentPage.createMessageComponentCollector({
|
||||||
|
time: MOD_MENU_TIMEOUT,
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("collect", async (i) => {
|
||||||
|
const opts = deserializeCustomId(i.customId);
|
||||||
|
if (opts.action == ModMenuActionType.PAGE) {
|
||||||
|
await i.deferUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update displayed embed if any navigation buttons were used
|
||||||
|
if (opts.action == ModMenuActionType.PAGE && opts.target == ModMenuNavigationType.INFO && infoEmbed != null) {
|
||||||
|
infoButton
|
||||||
|
.setLabel("Cases")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.CASES }));
|
||||||
|
firstButton.setDisabled(true);
|
||||||
|
prevButton.setDisabled(true);
|
||||||
|
nextButton.setDisabled(true);
|
||||||
|
lastButton.setDisabled(true);
|
||||||
|
|
||||||
|
await i.editReply({
|
||||||
|
embeds: [infoEmbed],
|
||||||
|
components: [navigationRow, moderationRow],
|
||||||
|
});
|
||||||
|
} else if (opts.action == ModMenuActionType.PAGE && opts.target == ModMenuNavigationType.CASES) {
|
||||||
|
infoButton
|
||||||
|
.setLabel("Info")
|
||||||
|
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.INFO }));
|
||||||
|
updateNavButtonState(firstButton, prevButton, nextButton, lastButton, page, totalPages);
|
||||||
|
|
||||||
|
await i.editReply({
|
||||||
|
embeds: [await loadPage(page)],
|
||||||
|
components: [navigationRow, moderationRow],
|
||||||
|
});
|
||||||
|
} else if (opts.action == ModMenuActionType.PAGE) {
|
||||||
|
let pageDelta = 0;
|
||||||
|
switch (opts.target) {
|
||||||
|
case ModMenuNavigationType.PREV:
|
||||||
|
pageDelta = -1;
|
||||||
|
break;
|
||||||
|
case ModMenuNavigationType.NEXT:
|
||||||
|
pageDelta = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newPage = 1;
|
||||||
|
if (opts.target == ModMenuNavigationType.PREV || opts.target == ModMenuNavigationType.NEXT) {
|
||||||
|
newPage = Math.max(Math.min(page + pageDelta, totalPages), 1);
|
||||||
|
} else if (opts.target == ModMenuNavigationType.FIRST) {
|
||||||
|
newPage = 1;
|
||||||
|
} else if (opts.target == ModMenuNavigationType.LAST) {
|
||||||
|
newPage = totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPage != page) {
|
||||||
|
updateNavButtonState(firstButton, prevButton, nextButton, lastButton, newPage, totalPages);
|
||||||
|
|
||||||
|
await i.editReply({
|
||||||
|
embeds: [await loadPage(newPage)],
|
||||||
|
components: [navigationRow, moderationRow],
|
||||||
|
});
|
||||||
|
|
||||||
|
page = newPage;
|
||||||
|
}
|
||||||
|
} else if (opts.action == ModMenuActionType.NOTE) {
|
||||||
|
await launchNoteActionModal(pluginData, i as ButtonInteraction, opts.target);
|
||||||
|
} else if (opts.action == ModMenuActionType.WARN) {
|
||||||
|
await launchWarnActionModal(pluginData, i as ButtonInteraction, opts.target);
|
||||||
|
} else if (opts.action == ModMenuActionType.CLEAN) {
|
||||||
|
await launchCleanActionModal(pluginData, i as ButtonInteraction, opts.target);
|
||||||
|
} else if (opts.action == ModMenuActionType.MUTE) {
|
||||||
|
await launchMuteActionModal(pluginData, i as ButtonInteraction, opts.target);
|
||||||
|
} else if (opts.action == ModMenuActionType.BAN) {
|
||||||
|
await launchBanActionModal(pluginData, i as ButtonInteraction, opts.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
collector.resetTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove components on timeout.
|
||||||
|
collector.on("end", async (_, reason) => {
|
||||||
|
if (reason !== "messageDelete") {
|
||||||
|
interaction.editReply({
|
||||||
|
components: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeCustomId(opts: ModMenuActionOpts) {
|
||||||
|
return `${opts.action}:${opts.target}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deserializeCustomId(customId: string): ModMenuActionOpts {
|
||||||
|
const opts: ModMenuActionOpts = {
|
||||||
|
action: customId.split(":")[0] as ModMenuActionType,
|
||||||
|
target: customId.split(":")[1],
|
||||||
|
};
|
||||||
|
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNavButtonState(
|
||||||
|
firstButton: ButtonBuilder,
|
||||||
|
prevButton: ButtonBuilder,
|
||||||
|
nextButton: ButtonBuilder,
|
||||||
|
lastButton: ButtonBuilder,
|
||||||
|
currentPage: number,
|
||||||
|
totalPages: number,
|
||||||
|
) {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
firstButton.setDisabled(false);
|
||||||
|
prevButton.setDisabled(false);
|
||||||
|
} else {
|
||||||
|
firstButton.setDisabled(true);
|
||||||
|
prevButton.setDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage == totalPages) {
|
||||||
|
nextButton.setDisabled(true);
|
||||||
|
lastButton.setDisabled(true);
|
||||||
|
} else {
|
||||||
|
nextButton.setDisabled(false);
|
||||||
|
lastButton.setDisabled(false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
import { contextMenuEvt } from "../types";
|
|
||||||
import { routeContextAction } from "../utils/contextRouter";
|
|
||||||
|
|
||||||
export const ContextClickedEvt = contextMenuEvt({
|
|
||||||
event: "interactionCreate",
|
|
||||||
|
|
||||||
async listener(meta) {
|
|
||||||
if (!meta.args.interaction.isContextMenuCommand()) return;
|
|
||||||
const inter = meta.args.interaction;
|
|
||||||
await routeContextAction(meta.pluginData, inter);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,25 +1,52 @@
|
||||||
|
import { APIEmbed, Awaitable } from "discord.js";
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { BasePluginType, guildPluginEventListener } from "knub";
|
import { BasePluginType } from "knub";
|
||||||
import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks";
|
import { GuildCases } from "../../data/GuildCases";
|
||||||
|
import { GuildLogs } from "../../data/GuildLogs";
|
||||||
|
import { GuildMutes } from "../../data/GuildMutes";
|
||||||
|
import { GuildTempbans } from "../../data/GuildTempbans";
|
||||||
|
import { tNullable } from "../../utils";
|
||||||
|
|
||||||
export const ConfigSchema = t.type({
|
export const ConfigSchema = t.type({
|
||||||
can_use: t.boolean,
|
can_use: t.boolean,
|
||||||
|
|
||||||
user_muteindef: t.boolean,
|
can_open_mod_menu: t.boolean,
|
||||||
user_mute1d: t.boolean,
|
|
||||||
user_mute1h: t.boolean,
|
log_channel: tNullable(t.string),
|
||||||
user_info: t.boolean,
|
|
||||||
message_clean10: t.boolean,
|
|
||||||
message_clean25: t.boolean,
|
|
||||||
message_clean50: t.boolean,
|
|
||||||
});
|
});
|
||||||
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||||
|
|
||||||
export interface ContextMenuPluginType extends BasePluginType {
|
export interface ContextMenuPluginType extends BasePluginType {
|
||||||
config: TConfigSchema;
|
config: TConfigSchema;
|
||||||
state: {
|
state: {
|
||||||
contextMenuLinks: GuildContextMenuLinks;
|
mutes: GuildMutes;
|
||||||
|
cases: GuildCases;
|
||||||
|
tempbans: GuildTempbans;
|
||||||
|
serverLogs: GuildLogs;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const contextMenuEvt = guildPluginEventListener<ContextMenuPluginType>();
|
export const enum ModMenuActionType {
|
||||||
|
PAGE = "page",
|
||||||
|
NOTE = "note",
|
||||||
|
WARN = "warn",
|
||||||
|
CLEAN = "clean",
|
||||||
|
MUTE = "mute",
|
||||||
|
BAN = "ban",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum ModMenuNavigationType {
|
||||||
|
FIRST = "first",
|
||||||
|
PREV = "prev",
|
||||||
|
NEXT = "next",
|
||||||
|
LAST = "last",
|
||||||
|
INFO = "info",
|
||||||
|
CASES = "cases",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModMenuActionOpts {
|
||||||
|
action: ModMenuActionType;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoadModMenuPageFn = (page: number) => Awaitable<APIEmbed>;
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { ContextMenuCommandInteraction } from "discord.js";
|
|
||||||
import { GuildPluginData } from "knub";
|
|
||||||
import { ContextMenuPluginType } from "../types";
|
|
||||||
import { hardcodedActions } from "./hardcodedContextOptions";
|
|
||||||
|
|
||||||
export async function routeContextAction(
|
|
||||||
pluginData: GuildPluginData<ContextMenuPluginType>,
|
|
||||||
interaction: ContextMenuCommandInteraction,
|
|
||||||
) {
|
|
||||||
const contextLink = await pluginData.state.contextMenuLinks.get(interaction.commandId);
|
|
||||||
if (!contextLink) return;
|
|
||||||
hardcodedActions[contextLink.action_name](pluginData, interaction);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { cleanAction } from "../actions/clean";
|
|
||||||
import { muteAction } from "../actions/mute";
|
|
||||||
import { userInfoAction } from "../actions/userInfo";
|
|
||||||
|
|
||||||
export const hardcodedContext: Record<string, string> = {
|
|
||||||
user_muteindef: "Mute Indefinitely",
|
|
||||||
user_mute1d: "Mute for 1 day",
|
|
||||||
user_mute1h: "Mute for 1 hour",
|
|
||||||
user_info: "Get Info",
|
|
||||||
message_clean10: "Clean 10 messages",
|
|
||||||
message_clean25: "Clean 25 messages",
|
|
||||||
message_clean50: "Clean 50 messages",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hardcodedActions = {
|
|
||||||
user_muteindef: (pluginData, interaction) => muteAction(pluginData, undefined, interaction),
|
|
||||||
user_mute1d: (pluginData, interaction) => muteAction(pluginData, "1d", interaction),
|
|
||||||
user_mute1h: (pluginData, interaction) => muteAction(pluginData, "1h", interaction),
|
|
||||||
user_info: (pluginData, interaction) => userInfoAction(pluginData, interaction),
|
|
||||||
message_clean10: (pluginData, interaction) => cleanAction(pluginData, 10, interaction),
|
|
||||||
message_clean25: (pluginData, interaction) => cleanAction(pluginData, 25, interaction),
|
|
||||||
message_clean50: (pluginData, interaction) => cleanAction(pluginData, 50, interaction),
|
|
||||||
};
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { ApplicationCommandData, ApplicationCommandType } from "discord.js";
|
|
||||||
import { GuildPluginData } from "knub";
|
|
||||||
import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin";
|
|
||||||
import { ContextMenuPluginType } from "../types";
|
|
||||||
import { hardcodedContext } from "./hardcodedContextOptions";
|
|
||||||
|
|
||||||
export async function loadAllCommands(pluginData: GuildPluginData<ContextMenuPluginType>) {
|
|
||||||
const comms = await pluginData.client.application!.commands;
|
|
||||||
const cfg = pluginData.config.get();
|
|
||||||
const newCommands: ApplicationCommandData[] = [];
|
|
||||||
const addedNames: string[] = [];
|
|
||||||
|
|
||||||
for (const [name, label] of Object.entries(hardcodedContext)) {
|
|
||||||
if (!cfg[name]) continue;
|
|
||||||
|
|
||||||
const type = name.startsWith("user") ? ApplicationCommandType.User : ApplicationCommandType.Message;
|
|
||||||
const data: ApplicationCommandData = {
|
|
||||||
type,
|
|
||||||
name: label,
|
|
||||||
};
|
|
||||||
|
|
||||||
addedNames.push(name);
|
|
||||||
newCommands.push(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const setCommands = await comms.set(newCommands, pluginData.guild.id).catch((e) => {
|
|
||||||
pluginData.getPlugin(LogsPlugin).logBotAlert({ body: `Unable to overwrite context menus: ${e}` });
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
if (!setCommands) return;
|
|
||||||
|
|
||||||
const setCommandsArray = [...setCommands.values()];
|
|
||||||
await pluginData.state.contextMenuLinks.deleteAll();
|
|
||||||
|
|
||||||
for (let i = 0; i < setCommandsArray.length; i++) {
|
|
||||||
const command = setCommandsArray[i];
|
|
||||||
pluginData.state.contextMenuLinks.create(command.id, addedNames[i]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -41,7 +41,12 @@ import { CreateUnbanCaseOnManualUnbanEvt } from "./events/CreateUnbanCaseOnManua
|
||||||
import { PostAlertOnMemberJoinEvt } from "./events/PostAlertOnMemberJoinEvt";
|
import { PostAlertOnMemberJoinEvt } from "./events/PostAlertOnMemberJoinEvt";
|
||||||
import { banUserId } from "./functions/banUserId";
|
import { banUserId } from "./functions/banUserId";
|
||||||
import { clearTempban } from "./functions/clearTempban";
|
import { clearTempban } from "./functions/clearTempban";
|
||||||
import { hasMutePermission } from "./functions/hasMutePerm";
|
import {
|
||||||
|
hasBanPermission,
|
||||||
|
hasMutePermission,
|
||||||
|
hasNotePermission,
|
||||||
|
hasWarnPermission,
|
||||||
|
} from "./functions/hasModActionPerm";
|
||||||
import { kickMember } from "./functions/kickMember";
|
import { kickMember } from "./functions/kickMember";
|
||||||
import { offModActionsEvent } from "./functions/offModActionsEvent";
|
import { offModActionsEvent } from "./functions/offModActionsEvent";
|
||||||
import { onModActionsEvent } from "./functions/onModActionsEvent";
|
import { onModActionsEvent } from "./functions/onModActionsEvent";
|
||||||
|
@ -158,7 +163,7 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({
|
||||||
public: {
|
public: {
|
||||||
warnMember(pluginData) {
|
warnMember(pluginData) {
|
||||||
return (member: GuildMember, reason: string, warnOptions?: WarnOptions) => {
|
return (member: GuildMember, reason: string, warnOptions?: WarnOptions) => {
|
||||||
warnMember(pluginData, member, reason, warnOptions);
|
return warnMember(pluginData, member, reason, warnOptions);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -170,7 +175,7 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({
|
||||||
|
|
||||||
banUserId(pluginData) {
|
banUserId(pluginData) {
|
||||||
return (userId: string, reason?: string, banOptions?: BanOptions, banTime?: number) => {
|
return (userId: string, reason?: string, banOptions?: BanOptions, banTime?: number) => {
|
||||||
banUserId(pluginData, userId, reason, banOptions, banTime);
|
return banUserId(pluginData, userId, reason, banOptions, banTime);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -180,12 +185,30 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasNotePermission(pluginData) {
|
||||||
|
return (member: GuildMember, channelId: Snowflake) => {
|
||||||
|
return hasNotePermission(pluginData, member, channelId);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
hasWarnPermission(pluginData) {
|
||||||
|
return (member: GuildMember, channelId: Snowflake) => {
|
||||||
|
return hasWarnPermission(pluginData, member, channelId);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
hasMutePermission(pluginData) {
|
hasMutePermission(pluginData) {
|
||||||
return (member: GuildMember, channelId: Snowflake) => {
|
return (member: GuildMember, channelId: Snowflake) => {
|
||||||
return hasMutePermission(pluginData, member, channelId);
|
return hasMutePermission(pluginData, member, channelId);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasBanPermission(pluginData) {
|
||||||
|
return (member: GuildMember, channelId: Snowflake) => {
|
||||||
|
return hasBanPermission(pluginData, member, channelId);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
on: mapToPublicFn(onModActionsEvent),
|
on: mapToPublicFn(onModActionsEvent),
|
||||||
off: mapToPublicFn(offModActionsEvent),
|
off: mapToPublicFn(offModActionsEvent),
|
||||||
getEventEmitter(pluginData) {
|
getEventEmitter(pluginData) {
|
||||||
|
|
35
backend/src/plugins/ModActions/functions/hasModActionPerm.ts
Normal file
35
backend/src/plugins/ModActions/functions/hasModActionPerm.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { GuildMember, Snowflake } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { ModActionsPluginType } from "../types";
|
||||||
|
|
||||||
|
export async function hasNotePermission(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
member: GuildMember,
|
||||||
|
channelId: Snowflake,
|
||||||
|
) {
|
||||||
|
return (await pluginData.config.getMatchingConfig({ member, channelId })).can_note;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasWarnPermission(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
member: GuildMember,
|
||||||
|
channelId: Snowflake,
|
||||||
|
) {
|
||||||
|
return (await pluginData.config.getMatchingConfig({ member, channelId })).can_warn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasMutePermission(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
member: GuildMember,
|
||||||
|
channelId: Snowflake,
|
||||||
|
) {
|
||||||
|
return (await pluginData.config.getMatchingConfig({ member, channelId })).can_mute;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasBanPermission(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
member: GuildMember,
|
||||||
|
channelId: Snowflake,
|
||||||
|
) {
|
||||||
|
return (await pluginData.config.getMatchingConfig({ member, channelId })).can_ban;
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
import { GuildMember, Snowflake } from "discord.js";
|
|
||||||
import { GuildPluginData } from "knub";
|
|
||||||
import { ModActionsPluginType } from "../types";
|
|
||||||
|
|
||||||
export async function hasMutePermission(
|
|
||||||
pluginData: GuildPluginData<ModActionsPluginType>,
|
|
||||||
member: GuildMember,
|
|
||||||
channelId: Snowflake,
|
|
||||||
) {
|
|
||||||
return (await pluginData.config.getMatchingConfig({ member, channelId })).can_mute;
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue