From 5e636b485acef4178f26965af872f96c8ab35413 Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Fri, 20 Nov 2020 02:31:31 +0100 Subject: [PATCH 1/6] Allow ban to be upgraded to forceban if the member is not on the server --- .../src/plugins/ModActions/commands/BanCmd.ts | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/backend/src/plugins/ModActions/commands/BanCmd.ts b/backend/src/plugins/ModActions/commands/BanCmd.ts index abd5fb68..38658e88 100644 --- a/backend/src/plugins/ModActions/commands/BanCmd.ts +++ b/backend/src/plugins/ModActions/commands/BanCmd.ts @@ -8,6 +8,7 @@ import { formatReasonWithAttachments } from "../functions/formatReasonWithAttach import { banUserId } from "../functions/banUserId"; import { ignoreEvent } from "../functions/ignoreEvent"; import { LogType } from "../../../data/LogType"; +import { waitForReaction } from "knub/dist/helpers"; const opts = { mod: ct.member({ option: true }), @@ -36,19 +37,30 @@ export const BanCmd = modActionsCmd({ const memberToBan = await resolveMember(pluginData.client, pluginData.guild, user.id); + let forceban = false; if (!memberToBan) { const banned = await isBanned(pluginData, user.id); + if (banned) { sendErrorMessage(pluginData, msg.channel, `User is already banned`); + return; } else { - sendErrorMessage(pluginData, msg.channel, `User not found on the server`); - } + // Ask the mod if we should upgrade to a forceban as the user is not on the server + const notOnServerMsg = await msg.channel.createMessage("User not found on the server, forceban instead?"); + const reply = await waitForReaction(pluginData.client, notOnServerMsg, ["✅", "❌"], msg.author.id); - return; + notOnServerMsg.delete(); + if (!reply || reply.name === "❌") { + sendErrorMessage(pluginData, msg.channel, "User not on server, ban cancelled by moderator"); + return; + } else { + forceban = true; + } + } } - // Make sure we're allowed to ban this member - if (!canActOn(pluginData, msg.member, memberToBan)) { + // Make sure we're allowed to ban this member if they are on the server + if (!forceban && !canActOn(pluginData, msg.member, memberToBan!)) { sendErrorMessage(pluginData, msg.channel, "Cannot ban: insufficient permissions"); return; } @@ -74,7 +86,7 @@ export const BanCmd = modActionsCmd({ const deleteMessageDays = args["delete-days"] ?? pluginData.config.getForMessage(msg).ban_delete_message_days; const reason = formatReasonWithAttachments(args.reason, msg.attachments); - const banResult = await banUserId(pluginData, memberToBan.id, reason, { + const banResult = await banUserId(pluginData, user.id, reason, { contactMethods, caseArgs: { modId: mod.id, @@ -89,9 +101,14 @@ export const BanCmd = modActionsCmd({ } // Confirm the action to the moderator - let response = `Banned **${memberToBan.user.username}#${memberToBan.user.discriminator}** (Case #${banResult.case.case_number})`; + let response = ""; + if (!forceban) { + response = `Banned **${user.username}#${user.discriminator}** (Case #${banResult.case.case_number})`; + if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`; + } else { + response = `Member forcebanned (Case #${banResult.case.case_number})`; + } - if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`; sendSuccessMessage(pluginData, msg.channel, response); }, }); From 7839cc7da17a809758bfcd333f43259857d6d042 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 14:14:17 +0200 Subject: [PATCH 2/6] Add 'make_role_mentionable' and 'make_role_unmentionable' custom event actions --- .../actions/makeRoleMentionableAction.ts | 44 +++++++++++++++++++ .../actions/makeRoleUnmentionableAction.ts | 30 +++++++++++++ .../CustomEvents/functions/runEvent.ts | 6 +++ backend/src/plugins/CustomEvents/types.ts | 11 ++++- 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts create mode 100644 backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts diff --git a/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts b/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts new file mode 100644 index 00000000..90ddd45b --- /dev/null +++ b/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts @@ -0,0 +1,44 @@ +import { GuildPluginData } from "knub"; +import { CustomEventsPluginType, TCustomEvent } from "../types"; +import * as t from "io-ts"; +import { convertDelayStringToMS, noop, tDelayString } from "../../../utils"; +import { ActionError } from "../ActionError"; + +export const MakeRoleMentionableAction = t.type({ + type: t.literal("make_role_mentionable"), + role: t.string, + timeout: tDelayString, +}); +export type TMakeRoleMentionableAction = t.TypeOf; + +export async function makeRoleMentionableAction( + pluginData: GuildPluginData, + action: TMakeRoleMentionableAction, + values: any, + event: TCustomEvent, + eventData: any, +) { + const role = pluginData.guild.roles.get(action.role); + if (!role) { + throw new ActionError(`Unknown role: ${role}`); + } + + await role.edit( + { + mentionable: true, + }, + `Custom event: ${event.name}`, + ); + + const timeout = convertDelayStringToMS(action.timeout)!; + setTimeout(() => { + role + .edit( + { + mentionable: false, + }, + `Custom event: ${event.name}`, + ) + .catch(noop); + }, timeout); +} diff --git a/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts b/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts new file mode 100644 index 00000000..0ceef330 --- /dev/null +++ b/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts @@ -0,0 +1,30 @@ +import { GuildPluginData } from "knub"; +import { CustomEventsPluginType, TCustomEvent } from "../types"; +import * as t from "io-ts"; +import { ActionError } from "../ActionError"; + +export const MakeRoleUnmentionableAction = t.type({ + type: t.literal("make_role_unmentionable"), + role: t.string, +}); +export type TMakeRoleUnmentionableAction = t.TypeOf; + +export async function makeRoleUnmentionableAction( + pluginData: GuildPluginData, + action: TMakeRoleUnmentionableAction, + values: any, + event: TCustomEvent, + eventData: any, +) { + const role = pluginData.guild.roles.get(action.role); + if (!role) { + throw new ActionError(`Unknown role: ${role}`); + } + + await role.edit( + { + mentionable: false, + }, + `Custom event: ${event.name}`, + ); +} diff --git a/backend/src/plugins/CustomEvents/functions/runEvent.ts b/backend/src/plugins/CustomEvents/functions/runEvent.ts index 56d3a9aa..bd0b3ef5 100644 --- a/backend/src/plugins/CustomEvents/functions/runEvent.ts +++ b/backend/src/plugins/CustomEvents/functions/runEvent.ts @@ -7,6 +7,8 @@ import { addRoleAction } from "../actions/addRoleAction"; import { createCaseAction } from "../actions/createCaseAction"; import { moveToVoiceChannelAction } from "../actions/moveToVoiceChannelAction"; import { messageAction } from "../actions/messageAction"; +import { makeRoleMentionableAction } from "../actions/makeRoleMentionableAction"; +import { makeRoleUnmentionableAction } from "../actions/makeRoleUnmentionableAction"; export async function runEvent( pluginData: GuildPluginData, @@ -24,6 +26,10 @@ export async function runEvent( await moveToVoiceChannelAction(pluginData, action, values, event, eventData); } else if (action.type === "message") { await messageAction(pluginData, action, values); + } else if (action.type === "make_role_mentionable") { + await makeRoleMentionableAction(pluginData, action, values, event, eventData); + } else if (action.type === "make_role_unmentionable") { + await makeRoleUnmentionableAction(pluginData, action, values, event, eventData); } } } catch (e) { diff --git a/backend/src/plugins/CustomEvents/types.ts b/backend/src/plugins/CustomEvents/types.ts index 7db0b14d..8bd0ffdf 100644 --- a/backend/src/plugins/CustomEvents/types.ts +++ b/backend/src/plugins/CustomEvents/types.ts @@ -4,6 +4,8 @@ import { AddRoleAction } from "./actions/addRoleAction"; import { CreateCaseAction } from "./actions/createCaseAction"; import { MoveToVoiceChannelAction } from "./actions/moveToVoiceChannelAction"; import { MessageAction } from "./actions/messageAction"; +import { MakeRoleMentionableAction } from "./actions/makeRoleMentionableAction"; +import { MakeRoleUnmentionableAction } from "./actions/makeRoleUnmentionableAction"; // Triggers const CommandTrigger = t.type({ @@ -17,7 +19,14 @@ type TCommandTrigger = t.TypeOf; const AnyTrigger = CommandTrigger; // TODO: Make into a union once we have more triggers type TAnyTrigger = t.TypeOf; -const AnyAction = t.union([AddRoleAction, CreateCaseAction, MoveToVoiceChannelAction, MessageAction]); +const AnyAction = t.union([ + AddRoleAction, + CreateCaseAction, + MoveToVoiceChannelAction, + MessageAction, + MakeRoleMentionableAction, + MakeRoleUnmentionableAction, +]); type TAnyAction = t.TypeOf; export const CustomEvent = t.type({ From e7efd485192eba3f4272c852bbd5096c86f7ff67 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 14:59:02 +0200 Subject: [PATCH 3/6] Add 'set_channel_permission_overrides' custom event action --- .../actions/setChannelPermissionOverrides.ts | 41 +++++++++++++++++++ .../CustomEvents/functions/runEvent.ts | 3 ++ backend/src/plugins/CustomEvents/types.ts | 2 + 3 files changed, 46 insertions(+) create mode 100644 backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts diff --git a/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts new file mode 100644 index 00000000..595a56e2 --- /dev/null +++ b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts @@ -0,0 +1,41 @@ +import { GuildPluginData } from "knub"; +import { CustomEventsPluginType, TCustomEvent } from "../types"; +import * as t from "io-ts"; +import { ActionError } from "../ActionError"; + +export const SetChannelPermissionOverridesAction = t.type({ + type: t.literal("set_channel_permission_overrides"), + channel: t.string, + overrides: t.array( + t.type({ + type: t.union([t.literal("member"), t.literal("role")]), + id: t.string, + allow: t.number, + deny: t.number, + }), + ), +}); +export type TSetChannelPermissionOverridesAction = t.TypeOf; + +export async function setChannelPermissionOverridesAction( + pluginData: GuildPluginData, + action: TSetChannelPermissionOverridesAction, + values: any, + event: TCustomEvent, + eventData: any, +) { + const channel = pluginData.guild.channels.get(action.channel); + if (!channel) { + throw new ActionError(`Unknown channel: ${action.channel}`); + } + + for (const override of action.overrides) { + await channel.editPermission( + override.id, + override.allow, + override.deny, + override.type, + `Custom event: ${event.name}`, + ); + } +} diff --git a/backend/src/plugins/CustomEvents/functions/runEvent.ts b/backend/src/plugins/CustomEvents/functions/runEvent.ts index bd0b3ef5..3b304ff9 100644 --- a/backend/src/plugins/CustomEvents/functions/runEvent.ts +++ b/backend/src/plugins/CustomEvents/functions/runEvent.ts @@ -9,6 +9,7 @@ import { moveToVoiceChannelAction } from "../actions/moveToVoiceChannelAction"; import { messageAction } from "../actions/messageAction"; import { makeRoleMentionableAction } from "../actions/makeRoleMentionableAction"; import { makeRoleUnmentionableAction } from "../actions/makeRoleUnmentionableAction"; +import { setChannelPermissionOverridesAction } from "../actions/setChannelPermissionOverrides"; export async function runEvent( pluginData: GuildPluginData, @@ -30,6 +31,8 @@ export async function runEvent( await makeRoleMentionableAction(pluginData, action, values, event, eventData); } else if (action.type === "make_role_unmentionable") { await makeRoleUnmentionableAction(pluginData, action, values, event, eventData); + } else if (action.type === "set_channel_permission_overrides") { + await setChannelPermissionOverridesAction(pluginData, action, values, event, eventData); } } } catch (e) { diff --git a/backend/src/plugins/CustomEvents/types.ts b/backend/src/plugins/CustomEvents/types.ts index 8bd0ffdf..44058777 100644 --- a/backend/src/plugins/CustomEvents/types.ts +++ b/backend/src/plugins/CustomEvents/types.ts @@ -6,6 +6,7 @@ import { MoveToVoiceChannelAction } from "./actions/moveToVoiceChannelAction"; import { MessageAction } from "./actions/messageAction"; import { MakeRoleMentionableAction } from "./actions/makeRoleMentionableAction"; import { MakeRoleUnmentionableAction } from "./actions/makeRoleUnmentionableAction"; +import { SetChannelPermissionOverridesAction } from "./actions/setChannelPermissionOverrides"; // Triggers const CommandTrigger = t.type({ @@ -26,6 +27,7 @@ const AnyAction = t.union([ MessageAction, MakeRoleMentionableAction, MakeRoleUnmentionableAction, + SetChannelPermissionOverridesAction, ]); type TAnyAction = t.TypeOf; From 2d749730a6c9c7c7d8572f1f729b6a68a6848922 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 3 Dec 2020 16:47:11 +0200 Subject: [PATCH 4/6] Fix automod mutes not using default contact methods --- backend/src/plugins/Automod/actions/mute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/Automod/actions/mute.ts b/backend/src/plugins/Automod/actions/mute.ts index d9d1ee25..324f763a 100644 --- a/backend/src/plugins/Automod/actions/mute.ts +++ b/backend/src/plugins/Automod/actions/mute.ts @@ -31,7 +31,7 @@ export const MuteAction = automodAction({ async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) { const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined; const reason = actionConfig.reason || "Muted automatically"; - const contactMethods = resolveActionContactMethods(pluginData, actionConfig); + const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; const caseArgs = { modId: pluginData.client.user.id, From 138bc05402c94d7f589feacba862056f6d1f3e57 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 3 Dec 2020 16:55:53 +0200 Subject: [PATCH 5/6] Fix automod warns, kicks, and bans not using default contact methods --- backend/src/plugins/Automod/actions/ban.ts | 2 +- backend/src/plugins/Automod/actions/kick.ts | 2 +- backend/src/plugins/Automod/actions/warn.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/plugins/Automod/actions/ban.ts b/backend/src/plugins/Automod/actions/ban.ts index fe15cc7e..0b53aff7 100644 --- a/backend/src/plugins/Automod/actions/ban.ts +++ b/backend/src/plugins/Automod/actions/ban.ts @@ -19,7 +19,7 @@ export const BanAction = automodAction({ async apply({ pluginData, contexts, actionConfig, matchResult }) { const reason = actionConfig.reason || "Kicked automatically"; - const contactMethods = resolveActionContactMethods(pluginData, actionConfig); + const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; const deleteMessageDays = actionConfig.deleteMessageDays || undefined; const caseArgs = { diff --git a/backend/src/plugins/Automod/actions/kick.ts b/backend/src/plugins/Automod/actions/kick.ts index 3a1f73c4..26f0dcc7 100644 --- a/backend/src/plugins/Automod/actions/kick.ts +++ b/backend/src/plugins/Automod/actions/kick.ts @@ -18,7 +18,7 @@ export const KickAction = automodAction({ async apply({ pluginData, contexts, actionConfig, matchResult }) { const reason = actionConfig.reason || "Kicked automatically"; - const contactMethods = resolveActionContactMethods(pluginData, actionConfig); + const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; const caseArgs = { modId: pluginData.client.user.id, diff --git a/backend/src/plugins/Automod/actions/warn.ts b/backend/src/plugins/Automod/actions/warn.ts index 781580b0..31a898b6 100644 --- a/backend/src/plugins/Automod/actions/warn.ts +++ b/backend/src/plugins/Automod/actions/warn.ts @@ -18,7 +18,7 @@ export const WarnAction = automodAction({ async apply({ pluginData, contexts, actionConfig, matchResult }) { const reason = actionConfig.reason || "Warned automatically"; - const contactMethods = resolveActionContactMethods(pluginData, actionConfig); + const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; const caseArgs = { modId: pluginData.client.user.id, From 809a72044f21fb859ea0f4526e5104b1c54fa623 Mon Sep 17 00:00:00 2001 From: Nils <7890309+DarkView@users.noreply.github.com> Date: Sat, 12 Dec 2020 21:15:15 +0100 Subject: [PATCH 6/6] Add command to move all VC Members to another VC (#126) --- backend/src/plugins/Utility/UtilityPlugin.ts | 3 +- .../src/plugins/Utility/commands/VcmoveCmd.ts | 99 ++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index 2f09cdc7..bfc08657 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -16,7 +16,7 @@ import { NicknameCmd } from "./commands/NicknameCmd"; import { PingCmd } from "./commands/PingCmd"; import { SourceCmd } from "./commands/SourceCmd"; import { ContextCmd } from "./commands/ContextCmd"; -import { VcmoveCmd } from "./commands/VcmoveCmd"; +import { VcmoveAllCmd, VcmoveCmd } from "./commands/VcmoveCmd"; import { HelpCmd } from "./commands/HelpCmd"; import { AboutCmd } from "./commands/AboutCmd"; import { PluginOptions } from "knub"; @@ -120,6 +120,7 @@ export const UtilityPlugin = zeppelinGuildPlugin()("utility", SourceCmd, ContextCmd, VcmoveCmd, + VcmoveAllCmd, HelpCmd, AboutCmd, ReloadGuildCmd, diff --git a/backend/src/plugins/Utility/commands/VcmoveCmd.ts b/backend/src/plugins/Utility/commands/VcmoveCmd.ts index af18c41d..b80ad810 100644 --- a/backend/src/plugins/Utility/commands/VcmoveCmd.ts +++ b/backend/src/plugins/Utility/commands/VcmoveCmd.ts @@ -4,11 +4,12 @@ import { channelMentionRegex, errorMessage, isSnowflake, + resolveMember, simpleClosestStringMatch, stripObjectToScalars, } from "../../../utils"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { VoiceChannel } from "eris"; +import { Member, VoiceChannel } from "eris"; import { LogType } from "../../../data/LogType"; export const VcmoveCmd = utilityCmd({ @@ -95,3 +96,99 @@ export const VcmoveCmd = utilityCmd({ ); }, }); + +export const VcmoveAllCmd = utilityCmd({ + trigger: "vcmoveall", + description: "Move all members of a voice channel to another voice channel", + usage: "!vcmoveall 551767166395875334 767497573560352798", + permission: "can_vcmove", + + signature: { + oldChannel: ct.voiceChannel(), + channel: ct.string({ catchAll: true }), + }, + + async run({ message: msg, args, pluginData }) { + let channel: VoiceChannel; + + if (isSnowflake(args.channel)) { + // Snowflake -> resolve channel directly + const potentialChannel = pluginData.guild.channels.get(args.channel); + if (!potentialChannel || !(potentialChannel instanceof VoiceChannel)) { + sendErrorMessage(pluginData, msg.channel, "Unknown or non-voice channel"); + return; + } + + channel = potentialChannel; + } else if (channelMentionRegex.test(args.channel)) { + // Channel mention -> parse channel id and resolve channel from that + const channelId = args.channel.match(channelMentionRegex)![1]; + const potentialChannel = pluginData.guild.channels.get(channelId); + if (!potentialChannel || !(potentialChannel instanceof VoiceChannel)) { + sendErrorMessage(pluginData, msg.channel, "Unknown or non-voice channel"); + return; + } + + channel = potentialChannel; + } else { + // Search string -> find closest matching voice channel name + const voiceChannels = pluginData.guild.channels.filter(theChannel => { + return theChannel instanceof VoiceChannel; + }) as VoiceChannel[]; + const closestMatch = simpleClosestStringMatch(args.channel, voiceChannels, ch => ch.name); + if (!closestMatch) { + sendErrorMessage(pluginData, msg.channel, "No matching voice channels"); + return; + } + + channel = closestMatch; + } + + if (args.oldChannel.voiceMembers.size === 0) { + sendErrorMessage(pluginData, msg.channel, "Voice channel is empty"); + return; + } + + if (args.oldChannel.id === channel.id) { + sendErrorMessage(pluginData, msg.channel, "Cant move from and to the same channel!"); + return; + } + + // Cant leave null, otherwise we get an assignment error in the catch + let currMember = msg.member; + const moveAmt = args.oldChannel.voiceMembers.size; + try { + for (const memberWithId of args.oldChannel.voiceMembers) { + currMember = memberWithId[1]; + + currMember.edit({ + channelID: channel.id, + }); + + pluginData.state.logs.log(LogType.VOICE_CHANNEL_FORCE_MOVE, { + mod: stripObjectToScalars(msg.author), + member: stripObjectToScalars(currMember, ["user", "roles"]), + oldChannel: stripObjectToScalars(args.oldChannel), + newChannel: stripObjectToScalars(channel), + }); + } + } catch (e) { + if (msg.member.id === currMember.id) { + sendErrorMessage(pluginData, msg.channel, "Unknown error when trying to move members"); + return; + } + sendErrorMessage( + pluginData, + msg.channel, + `Failed to move ${currMember.username}#${currMember.discriminator} (${currMember.id})`, + ); + return; + } + + sendSuccessMessage( + pluginData, + msg.channel, + `All ${moveAmt} members from **${args.oldChannel.name}** moved to **${channel.name}**`, + ); + }, +});