From 6d4a7cdafdeb04c83d8f948824a4dea6508a3969 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 7 Aug 2020 01:21:31 +0300 Subject: [PATCH] More work on permission utils and eager permission checks --- .../AutoReactions/events/AddReactionsEvt.ts | 20 +++---- .../src/plugins/Automod/actions/addRoles.ts | 48 +++++++++++++++-- .../plugins/Automod/actions/removeRoles.ts | 43 ++++++++++++++- backend/src/utils/canAssignRole.ts | 28 ++++++++++ backend/src/utils/canReadChannel.ts | 5 +- ...ons.ts => getMissingChannelPermissions.ts} | 10 ++-- backend/src/utils/getMissingPermissions.ts | 17 ++++++ backend/src/utils/getPermissionNames.ts | 29 ++++++++++ backend/src/utils/missingPermissionError.ts | 6 +++ backend/src/utils/readChannelPermissions.ts | 8 ++- backend/src/utils/verifyPermissions.ts | 54 ------------------- 11 files changed, 189 insertions(+), 79 deletions(-) create mode 100644 backend/src/utils/canAssignRole.ts rename backend/src/utils/{memberHasChannelPermissions.ts => getMissingChannelPermissions.ts} (57%) create mode 100644 backend/src/utils/getMissingPermissions.ts create mode 100644 backend/src/utils/getPermissionNames.ts create mode 100644 backend/src/utils/missingPermissionError.ts delete mode 100644 backend/src/utils/verifyPermissions.ts diff --git a/backend/src/plugins/AutoReactions/events/AddReactionsEvt.ts b/backend/src/plugins/AutoReactions/events/AddReactionsEvt.ts index ccdf5533..7e47a53f 100644 --- a/backend/src/plugins/AutoReactions/events/AddReactionsEvt.ts +++ b/backend/src/plugins/AutoReactions/events/AddReactionsEvt.ts @@ -4,7 +4,9 @@ import { LogType } from "src/data/LogType"; import { logger } from "../../../logger"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { Constants, GuildChannel } from "eris"; -import { memberHasChannelPermissions } from "../../../utils/memberHasChannelPermissions"; +import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions"; +import { readChannelPermissions } from "../../../utils/readChannelPermissions"; +import { missingPermissionError } from "../../../utils/missingPermissionError"; const p = Constants.Permissions; @@ -17,16 +19,16 @@ export const AddReactionsEvt = autoReactionsEvt({ const autoReaction = await pluginData.state.autoReactions.getForChannel(message.channel.id); if (!autoReaction) return; - if ( - !memberHasChannelPermissions(message.member, message.channel as GuildChannel, [ - p.readMessages, - p.readMessageHistory, - p.addReactions, - ]) - ) { + const me = pluginData.guild.members.get(pluginData.client.user.id); + const missingPermissions = getMissingChannelPermissions( + me, + message.channel as GuildChannel, + readChannelPermissions | p.addReactions, + ); + if (missingPermissions) { const logs = pluginData.getPlugin(LogsPlugin); logs.log(LogType.BOT_ALERT, { - body: `Missing permissions to apply auto-reactions in <#${message.channel.id}>. Ensure I can read messages, read message history, and add reactions.`, + body: `Cannot apply auto-reactions in <#${message.channel.id}>. ${missingPermissionError(missingPermissions)}`, }); return; } diff --git a/backend/src/plugins/Automod/actions/addRoles.ts b/backend/src/plugins/Automod/actions/addRoles.ts index 0c48f2ac..5f8e8a3b 100644 --- a/backend/src/plugins/Automod/actions/addRoles.ts +++ b/backend/src/plugins/Automod/actions/addRoles.ts @@ -1,21 +1,59 @@ import * as t from "io-ts"; import { automodAction } from "../helpers"; import { LogType } from "../../../data/LogType"; -import { asyncMap, resolveMember, tNullable, unique } from "../../../utils"; -import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; -import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; +import { unique } from "../../../utils"; +import { Constants } from "eris"; +import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions"; +import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { getMissingPermissions } from "../../../utils/getMissingPermissions"; +import { canAssignRole } from "../../../utils/canAssignRole"; +import { missingPermissionError } from "../../../utils/missingPermissionError"; + +const p = Constants.Permissions; export const AddRolesAction = automodAction({ configType: t.array(t.string), defaultConfig: [], - async apply({ pluginData, contexts, actionConfig }) { + async apply({ pluginData, contexts, actionConfig, ruleName }) { const members = unique(contexts.map(c => c.member).filter(Boolean)); + const me = pluginData.guild.members.get(pluginData.client.user.id); + + const missingPermissions = getMissingPermissions(me.permission, p.manageRoles); + if (missingPermissions) { + const logs = pluginData.getPlugin(LogsPlugin); + logs.log(LogType.BOT_ALERT, { + body: `Cannot add roles in Automod rule **${ruleName}**. ${missingPermissionError(missingPermissions)}`, + }); + return; + } + + const rolesToAssign = []; + const rolesWeCannotAssign = []; + for (const roleId of actionConfig) { + if (canAssignRole(pluginData.guild, me, roleId)) { + rolesToAssign.push(roleId); + } else { + rolesWeCannotAssign.push(roleId); + } + } + + if (rolesWeCannotAssign.length) { + const roleNamesWeCannotAssign = rolesWeCannotAssign.map( + roleId => pluginData.guild.roles.get(roleId)?.name || roleId, + ); + const logs = pluginData.getPlugin(LogsPlugin); + logs.log(LogType.BOT_ALERT, { + body: `Unable to assign the following roles in Automod rule **${ruleName}**: **${roleNamesWeCannotAssign.join( + "**, **", + )}**`, + }); + } await Promise.all( members.map(async member => { const memberRoles = new Set(member.roles); - for (const roleId of actionConfig) { + for (const roleId of rolesToAssign) { memberRoles.add(roleId); } diff --git a/backend/src/plugins/Automod/actions/removeRoles.ts b/backend/src/plugins/Automod/actions/removeRoles.ts index 2f824435..614a12bf 100644 --- a/backend/src/plugins/Automod/actions/removeRoles.ts +++ b/backend/src/plugins/Automod/actions/removeRoles.ts @@ -4,19 +4,58 @@ import { LogType } from "../../../data/LogType"; import { asyncMap, resolveMember, tNullable, unique } from "../../../utils"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; +import { getMissingPermissions } from "../../../utils/getMissingPermissions"; +import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { missingPermissionError } from "../../../utils/missingPermissionError"; +import { canAssignRole } from "../../../utils/canAssignRole"; +import { Constants } from "eris"; + +const p = Constants.Permissions; export const RemoveRolesAction = automodAction({ configType: t.array(t.string), defaultConfig: [], - async apply({ pluginData, contexts, actionConfig }) { + async apply({ pluginData, contexts, actionConfig, ruleName }) { const members = unique(contexts.map(c => c.member).filter(Boolean)); + const me = pluginData.guild.members.get(pluginData.client.user.id); + + const missingPermissions = getMissingPermissions(me.permission, p.manageRoles); + if (missingPermissions) { + const logs = pluginData.getPlugin(LogsPlugin); + logs.log(LogType.BOT_ALERT, { + body: `Cannot add roles in Automod rule **${ruleName}**. ${missingPermissionError(missingPermissions)}`, + }); + return; + } + + const rolesToRemove = []; + const rolesWeCannotRemove = []; + for (const roleId of actionConfig) { + if (canAssignRole(pluginData.guild, me, roleId)) { + rolesToRemove.push(roleId); + } else { + rolesWeCannotRemove.push(roleId); + } + } + + if (rolesWeCannotRemove.length) { + const roleNamesWeCannotRemove = rolesWeCannotRemove.map( + roleId => pluginData.guild.roles.get(roleId)?.name || roleId, + ); + const logs = pluginData.getPlugin(LogsPlugin); + logs.log(LogType.BOT_ALERT, { + body: `Unable to remove the following roles in Automod rule **${ruleName}**: **${roleNamesWeCannotRemove.join( + "**, **", + )}**`, + }); + } await Promise.all( members.map(async member => { const memberRoles = new Set(member.roles); - for (const roleId of actionConfig) { + for (const roleId of rolesToRemove) { memberRoles.delete(roleId); } diff --git a/backend/src/utils/canAssignRole.ts b/backend/src/utils/canAssignRole.ts new file mode 100644 index 00000000..e7d7d514 --- /dev/null +++ b/backend/src/utils/canAssignRole.ts @@ -0,0 +1,28 @@ +import { Constants, Guild, Member, Role } from "eris"; +import { getMissingPermissions } from "./getMissingPermissions"; +import { hasDiscordPermissions } from "./hasDiscordPermissions"; + +export function canAssignRole(guild: Guild, member: Member, roleId: string) { + if (getMissingPermissions(member.permission, Constants.Permissions.manageRoles)) { + return false; + } + + if (roleId === guild.id) { + return false; + } + + const targetRole = guild.roles.get(roleId); + if (!targetRole) { + return false; + } + + const memberRoles = member.roles.map(_roleId => guild.roles.get(_roleId)); + const highestRoleWithManageRoles: Role = memberRoles.reduce((highest, role) => { + if (!hasDiscordPermissions(role.permissions, Constants.Permissions.manageRoles)) return highest; + if (highest == null) return role; + if (role.position > highest.position) return role; + return highest; + }, null); + + return highestRoleWithManageRoles.position > targetRole.position; +} diff --git a/backend/src/utils/canReadChannel.ts b/backend/src/utils/canReadChannel.ts index f27823e4..fb14874a 100644 --- a/backend/src/utils/canReadChannel.ts +++ b/backend/src/utils/canReadChannel.ts @@ -1,7 +1,8 @@ import { Constants, GuildChannel, Member } from "eris"; -import { memberHasChannelPermissions } from "./memberHasChannelPermissions"; import { readChannelPermissions } from "./readChannelPermissions"; +import { getMissingChannelPermissions } from "./getMissingChannelPermissions"; export function canReadChannel(channel: GuildChannel, member: Member) { - return memberHasChannelPermissions(member, channel, readChannelPermissions); + // Not missing permissions required to read the channel = can read channel + return !getMissingChannelPermissions(member, channel, readChannelPermissions); } diff --git a/backend/src/utils/memberHasChannelPermissions.ts b/backend/src/utils/getMissingChannelPermissions.ts similarity index 57% rename from backend/src/utils/memberHasChannelPermissions.ts rename to backend/src/utils/getMissingChannelPermissions.ts index c8707b72..4701ddb4 100644 --- a/backend/src/utils/memberHasChannelPermissions.ts +++ b/backend/src/utils/getMissingChannelPermissions.ts @@ -1,15 +1,15 @@ import { Constants, GuildChannel, Member, Permission } from "eris"; -import { PluginData } from "knub"; -import { hasDiscordPermissions } from "./hasDiscordPermissions"; +import { getMissingPermissions } from "./getMissingPermissions"; /** * @param requiredPermissions Bitmask of required permissions + * @return Bitmask of missing permissions */ -export function memberHasChannelPermissions( +export function getMissingChannelPermissions( member: Member, channel: GuildChannel, requiredPermissions: number | bigint, -) { +): bigint { const memberChannelPermissions = channel.permissionsOf(member.id); - return hasDiscordPermissions(memberChannelPermissions, requiredPermissions); + return getMissingPermissions(memberChannelPermissions, requiredPermissions); } diff --git a/backend/src/utils/getMissingPermissions.ts b/backend/src/utils/getMissingPermissions.ts new file mode 100644 index 00000000..9dd6881a --- /dev/null +++ b/backend/src/utils/getMissingPermissions.ts @@ -0,0 +1,17 @@ +import { Constants, Permission } from "eris"; + +/** + * @param resolvedPermissions A Permission object from e.g. GuildChannel#permissionsOf() or Member#permission + * @param requiredPermissions Bitmask of required permissions + * @return Bitmask of missing permissions + */ +export function getMissingPermissions(resolvedPermissions: Permission, requiredPermissions: number | bigint): bigint { + const allowedPermissions = BigInt(resolvedPermissions.allow); + const nRequiredPermissions = BigInt(requiredPermissions); + + if (Boolean(allowedPermissions & BigInt(Constants.Permissions.administrator))) { + return BigInt(0); + } + + return nRequiredPermissions & ~allowedPermissions; +} diff --git a/backend/src/utils/getPermissionNames.ts b/backend/src/utils/getPermissionNames.ts new file mode 100644 index 00000000..fecef3c5 --- /dev/null +++ b/backend/src/utils/getPermissionNames.ts @@ -0,0 +1,29 @@ +import { Constants } from "eris"; + +const camelCaseToTitleCase = str => + str + .replace(/([a-z])([A-Z])/g, "$1 $2") + .split(" ") + .map(w => w[0].toUpperCase() + w.slice(1)) + .join(" "); + +const permissionNumberToName: Map = new Map(); +const ignoredPermissionConstants = ["all", "allGuild", "allText", "allVoice"]; + +for (const key in Constants.Permissions) { + if (ignoredPermissionConstants.includes(key)) continue; + permissionNumberToName.set(BigInt(Constants.Permissions[key]), camelCaseToTitleCase(key)); +} + +/** + * @param permissions Bitmask of permissions to get the names for + */ +export function getPermissionNames(permissions: number | bigint): string[] { + const permissionNames = []; + for (const [permissionNumber, permissionName] of permissionNumberToName.entries()) { + if (BigInt(permissions) & permissionNumber) { + permissionNames.push(permissionName); + } + } + return permissionNames; +} diff --git a/backend/src/utils/missingPermissionError.ts b/backend/src/utils/missingPermissionError.ts new file mode 100644 index 00000000..e19ecb9b --- /dev/null +++ b/backend/src/utils/missingPermissionError.ts @@ -0,0 +1,6 @@ +import { getPermissionNames } from "./getPermissionNames"; + +export function missingPermissionError(missingPermissions: number | bigint): string { + const permissionNames = getPermissionNames(missingPermissions); + return `Missing permissions: **${permissionNames.join("**, **")}**`; +} diff --git a/backend/src/utils/readChannelPermissions.ts b/backend/src/utils/readChannelPermissions.ts index 21120c51..fdfb4aac 100644 --- a/backend/src/utils/readChannelPermissions.ts +++ b/backend/src/utils/readChannelPermissions.ts @@ -3,5 +3,9 @@ import { Constants } from "eris"; /** * Bitmask of permissions required to read messages in a channel */ -export const readChannelPermissions = - BigInt(Constants.Permissions.readMessages) | BigInt(Constants.Permissions.readMessageHistory); +export const readChannelPermissions = Constants.Permissions.readMessages | Constants.Permissions.readMessageHistory; + +/** + * Bitmask of permissions required to read messages in a channel (bigint) + */ +export const nReadChannelPermissions = BigInt(readChannelPermissions); diff --git a/backend/src/utils/verifyPermissions.ts b/backend/src/utils/verifyPermissions.ts deleted file mode 100644 index e6f8088b..00000000 --- a/backend/src/utils/verifyPermissions.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Constants, Permission } from "eris"; -import { PluginData } from "knub"; -import { hasDiscordPermissions } from "./hasDiscordPermissions"; -import { LogsPlugin } from "../plugins/Logs/LogsPlugin"; -import { LogType } from "../data/LogType"; - -const defaultErrorText = `Missing permissions.`; - -const camelCaseToTitleCase = str => - str - .replace(/([a-z])([A-Z])/g, "$1 $2") - .split(" ") - .map(w => w[0].toUpperCase() + w.slice(1)) - .join(" "); - -const permissionNumberToName: Map = new Map(); -for (const key in Constants.Permissions) { - permissionNumberToName.set(BigInt(Constants.Permissions[key]), camelCaseToTitleCase(key)); -} - -/** - * - * @param resolvedPermissions A Permission object from e.g. GuildChannel#permissionsOf() or Member#permission - * @param requiredPermissions Bitmask of required permissions - * @param errorText Custom error text - */ -export function verifyPermissions( - pluginData: PluginData, - resolvedPermissions: Permission, - requiredPermissions: number | bigint, - errorText?: string, -) { - const nRequiredPermissions = BigInt(requiredPermissions); - - if (!hasDiscordPermissions(resolvedPermissions, nRequiredPermissions)) { - const requiredPermissionNames = []; - for (const [permissionNumber, permissionName] of permissionNumberToName.entries()) { - if (nRequiredPermissions & permissionNumber) { - requiredPermissionNames.push(permissionName); - } - } - - const logs = pluginData.getPlugin(LogsPlugin); - logs.log(LogType.BOT_ALERT, { - body: `${errorText || - defaultErrorText} Please ensure I have the following permissions: **${requiredPermissionNames.join( - "**, **", - )}**`.trim(), - }); - return false; - } - - return true; -}