More work on permission utils and eager permission checks

This commit is contained in:
Dragory 2020-08-07 01:21:31 +03:00
parent 8af64a6944
commit 6d4a7cdafd
No known key found for this signature in database
GPG key ID: 5F387BA66DF8AAC1
11 changed files with 189 additions and 79 deletions

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
}

View 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;
}

View file

@ -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);
}

View file

@ -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);
}

View 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;
}

View 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;
}

View 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("**, **")}**`;
}

View file

@ -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);

View file

@ -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;
}