More work on permission utils and eager permission checks
This commit is contained in:
parent
8af64a6944
commit
6d4a7cdafd
11 changed files with 189 additions and 79 deletions
|
@ -4,7 +4,9 @@ import { LogType } from "src/data/LogType";
|
||||||
import { logger } from "../../../logger";
|
import { logger } from "../../../logger";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
import { Constants, GuildChannel } from "eris";
|
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;
|
const p = Constants.Permissions;
|
||||||
|
|
||||||
|
@ -17,16 +19,16 @@ export const AddReactionsEvt = autoReactionsEvt({
|
||||||
const autoReaction = await pluginData.state.autoReactions.getForChannel(message.channel.id);
|
const autoReaction = await pluginData.state.autoReactions.getForChannel(message.channel.id);
|
||||||
if (!autoReaction) return;
|
if (!autoReaction) return;
|
||||||
|
|
||||||
if (
|
const me = pluginData.guild.members.get(pluginData.client.user.id);
|
||||||
!memberHasChannelPermissions(message.member, message.channel as GuildChannel, [
|
const missingPermissions = getMissingChannelPermissions(
|
||||||
p.readMessages,
|
me,
|
||||||
p.readMessageHistory,
|
message.channel as GuildChannel,
|
||||||
p.addReactions,
|
readChannelPermissions | p.addReactions,
|
||||||
])
|
);
|
||||||
) {
|
if (missingPermissions) {
|
||||||
const logs = pluginData.getPlugin(LogsPlugin);
|
const logs = pluginData.getPlugin(LogsPlugin);
|
||||||
logs.log(LogType.BOT_ALERT, {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,59 @@
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { automodAction } from "../helpers";
|
import { automodAction } from "../helpers";
|
||||||
import { LogType } from "../../../data/LogType";
|
import { LogType } from "../../../data/LogType";
|
||||||
import { asyncMap, resolveMember, tNullable, unique } from "../../../utils";
|
import { unique } from "../../../utils";
|
||||||
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
|
import { Constants } from "eris";
|
||||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
|
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({
|
export const AddRolesAction = automodAction({
|
||||||
configType: t.array(t.string),
|
configType: t.array(t.string),
|
||||||
defaultConfig: [],
|
defaultConfig: [],
|
||||||
|
|
||||||
async apply({ pluginData, contexts, actionConfig }) {
|
async apply({ pluginData, contexts, actionConfig, ruleName }) {
|
||||||
const members = unique(contexts.map(c => c.member).filter(Boolean));
|
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(
|
await Promise.all(
|
||||||
members.map(async member => {
|
members.map(async member => {
|
||||||
const memberRoles = new Set(member.roles);
|
const memberRoles = new Set(member.roles);
|
||||||
for (const roleId of actionConfig) {
|
for (const roleId of rolesToAssign) {
|
||||||
memberRoles.add(roleId);
|
memberRoles.add(roleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,19 +4,58 @@ import { LogType } from "../../../data/LogType";
|
||||||
import { asyncMap, resolveMember, tNullable, unique } from "../../../utils";
|
import { asyncMap, resolveMember, tNullable, unique } from "../../../utils";
|
||||||
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
|
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
|
||||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
|
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({
|
export const RemoveRolesAction = automodAction({
|
||||||
configType: t.array(t.string),
|
configType: t.array(t.string),
|
||||||
|
|
||||||
defaultConfig: [],
|
defaultConfig: [],
|
||||||
|
|
||||||
async apply({ pluginData, contexts, actionConfig }) {
|
async apply({ pluginData, contexts, actionConfig, ruleName }) {
|
||||||
const members = unique(contexts.map(c => c.member).filter(Boolean));
|
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(
|
await Promise.all(
|
||||||
members.map(async member => {
|
members.map(async member => {
|
||||||
const memberRoles = new Set(member.roles);
|
const memberRoles = new Set(member.roles);
|
||||||
for (const roleId of actionConfig) {
|
for (const roleId of rolesToRemove) {
|
||||||
memberRoles.delete(roleId);
|
memberRoles.delete(roleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
28
backend/src/utils/canAssignRole.ts
Normal file
28
backend/src/utils/canAssignRole.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import { Constants, GuildChannel, Member } from "eris";
|
import { Constants, GuildChannel, Member } from "eris";
|
||||||
import { memberHasChannelPermissions } from "./memberHasChannelPermissions";
|
|
||||||
import { readChannelPermissions } from "./readChannelPermissions";
|
import { readChannelPermissions } from "./readChannelPermissions";
|
||||||
|
import { getMissingChannelPermissions } from "./getMissingChannelPermissions";
|
||||||
|
|
||||||
export function canReadChannel(channel: GuildChannel, member: Member) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { Constants, GuildChannel, Member, Permission } from "eris";
|
import { Constants, GuildChannel, Member, Permission } from "eris";
|
||||||
import { PluginData } from "knub";
|
import { getMissingPermissions } from "./getMissingPermissions";
|
||||||
import { hasDiscordPermissions } from "./hasDiscordPermissions";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param requiredPermissions Bitmask of required permissions
|
* @param requiredPermissions Bitmask of required permissions
|
||||||
|
* @return Bitmask of missing permissions
|
||||||
*/
|
*/
|
||||||
export function memberHasChannelPermissions(
|
export function getMissingChannelPermissions(
|
||||||
member: Member,
|
member: Member,
|
||||||
channel: GuildChannel,
|
channel: GuildChannel,
|
||||||
requiredPermissions: number | bigint,
|
requiredPermissions: number | bigint,
|
||||||
) {
|
): bigint {
|
||||||
const memberChannelPermissions = channel.permissionsOf(member.id);
|
const memberChannelPermissions = channel.permissionsOf(member.id);
|
||||||
return hasDiscordPermissions(memberChannelPermissions, requiredPermissions);
|
return getMissingPermissions(memberChannelPermissions, requiredPermissions);
|
||||||
}
|
}
|
17
backend/src/utils/getMissingPermissions.ts
Normal file
17
backend/src/utils/getMissingPermissions.ts
Normal file
|
@ -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;
|
||||||
|
}
|
29
backend/src/utils/getPermissionNames.ts
Normal file
29
backend/src/utils/getPermissionNames.ts
Normal file
|
@ -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<bigint, string> = 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;
|
||||||
|
}
|
6
backend/src/utils/missingPermissionError.ts
Normal file
6
backend/src/utils/missingPermissionError.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { getPermissionNames } from "./getPermissionNames";
|
||||||
|
|
||||||
|
export function missingPermissionError(missingPermissions: number | bigint): string {
|
||||||
|
const permissionNames = getPermissionNames(missingPermissions);
|
||||||
|
return `Missing permissions: **${permissionNames.join("**, **")}**`;
|
||||||
|
}
|
|
@ -3,5 +3,9 @@ import { Constants } from "eris";
|
||||||
/**
|
/**
|
||||||
* Bitmask of permissions required to read messages in a channel
|
* Bitmask of permissions required to read messages in a channel
|
||||||
*/
|
*/
|
||||||
export const readChannelPermissions =
|
export const readChannelPermissions = Constants.Permissions.readMessages | Constants.Permissions.readMessageHistory;
|
||||||
BigInt(Constants.Permissions.readMessages) | BigInt(Constants.Permissions.readMessageHistory);
|
|
||||||
|
/**
|
||||||
|
* Bitmask of permissions required to read messages in a channel (bigint)
|
||||||
|
*/
|
||||||
|
export const nReadChannelPermissions = BigInt(readChannelPermissions);
|
||||||
|
|
|
@ -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<bigint, string> = 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<any>,
|
|
||||||
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;
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue