feat(util/permissions): improve permission-checking (#381)

* Update permissions.ts

* add(permissions): explaining comments

Since Discord permissions are quiet complex  it is better to have detailed comments explaining everything.

* docs: add better permissions jsdoc comments

* types: add missing errors

* change imports

* we want a string here

* strange commit here

* we need an s in tts

* permissions: update channel permission handling

* permissions: update guild permission handling

* permissions: update member permission handling

* permissions: update message permission handling

* permissions: update webhook permission handling

* fix this buggg

* fix: typo

* better func names

* better description

* permissions(editMember): add permission check if channel_id is provided

* added todo for deaf

* fixxx

* FIIIXXX

* Update permissions.ts

* throwOn to require

* change up review things

* Update src/util/permissions.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update src/util/permissions.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update src/util/permissions.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update src/util/permissions.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update src/util/permissions.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Apply suggestions from code review

Co-authored-by: Ayyan <ayyantee@gmail.com>

* BigInt() to n

* Update src/util/permissions.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update src/util/permissions.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update src/util/permissions.ts

Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>

* missed this

* Update permissions.ts

* here enum is needed

* use set so errors arenn't strange

* dumb idea

* hasChannelPermissions functions are nice to have

* role to guild

* bugg

* fix(handlers): createGuildChannel check overwrite perms

* remove redundant if check

* fixes

* Update guild.ts

* bettrrrr

* Revert "bettrrrr"

This reverts commit ecbd30e160.

* I hate it

* fix fix

* fixxesss

* this function is better

* oh forgot these

* better I guess

* more functions

* silly me forgot to remove console.logs

* buuuuugs

* small changes

* Update permission.ts

* Update permissions.ts

* Update GUILD_CREATE.ts

* Update channel.ts

* remove this

* suggestions

Co-authored-by: Ayyan <ayyantee@gmail.com>
Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>
This commit is contained in:
ITOH
2021-03-11 18:33:52 +00:00
committed by GitHub
parent d9e994b4d8
commit ebbcd762cf
9 changed files with 605 additions and 877 deletions
+270 -177
View File
@@ -1,249 +1,328 @@
import { cacheHandlers } from "../cache.ts";
import { Guild, Role } from "../structures/mod.ts";
import { botID } from "../bot.ts";
import { Permission, Permissions, RawOverwrite } from "../types/mod.ts";
import { cacheHandlers } from "../cache.ts";
import { Channel, Guild, Member, Role } from "../structures/mod.ts";
import { Errors, Permission, Permissions } from "../types/mod.ts";
/** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */
export async function memberIDHasPermission(
memberID: string,
guildID: string,
permissions: Permission[],
async function getCached(
table: "guilds",
key: string | Guild,
): Promise<Guild>;
async function getCached(
table: "channels",
key: string | Channel,
): Promise<Channel>;
async function getCached(
table: "members",
key: string | Member,
): Promise<Member>;
async function getCached(
table: "guilds" | "channels" | "members",
key: string | Guild | Channel | Member,
) {
const guild = await cacheHandlers.get("guilds", guildID);
if (!guild) return false;
const cached = typeof key === "string"
? // @ts-ignore TS is wrong here
(await cacheHandlers.get(table, key))
: key;
if (!cached || typeof cached === "string") {
throw new Error(
Errors[`${table.slice(0, -1).toUpperCase()}_NOT_FOUND` as Errors],
);
}
if (memberID === guild.ownerID) return true;
const member = (await cacheHandlers.get("members", memberID))?.guilds.get(
guildID,
);
if (!member) return false;
return memberHasPermission(memberID, guild, member.roles, permissions);
return cached;
}
/** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */
export function memberHasPermission(
memberID: string,
guild: Guild,
memberRoleIDs: string[],
permissions: Permission[],
/** Calculates the permissions this member has in the given guild */
export async function calculateBasePermissions(
guild: string | Guild,
member: string | Member,
) {
if (memberID === guild.ownerID) return true;
guild = await getCached("guilds", guild);
member = await getCached("members", member);
const permissionBits = [guild.id, ...memberRoleIDs].map((id) =>
guild.roles.get(id)?.permissions
)
let permissions = 0n;
// Calculate the role permissions bits, @everyone role is not in memberRoleIDs so we need to pass guildID manualy
permissions |= [...(member.guilds.get(guild.id)?.roles || []), guild.id]
.map((id) => (guild as Guild).roles.get(id)?.permissions)
// Removes any edge case undefined
.filter((id) => id)
.filter((perm) => perm)
.reduce((bits, perms) => {
bits |= BigInt(perms);
return bits;
}, BigInt(0));
}, 0n);
if (permissionBits & BigInt(Permissions.ADMINISTRATOR)) return true;
// If the memberID is equal to the guild ownerID he automatically has every permission so we add ADMINISTRATOR permission
if (guild.ownerID === member.id) permissions |= 8n;
// Return the members permission bits as a string
return permissions.toString();
}
return permissions.every((permission) =>
permissionBits & BigInt(Permissions[permission])
/** Calculates the permissions this member has for the given Channel */
export async function calculateChannelOverwrites(
channel: string | Channel,
member: string | Member,
) {
channel = await getCached("channels", channel);
// This is a DM channel so return ADMINISTRATOR permission
if (!channel.guildID) return "8";
member = await getCached("members", member);
// Get all the role permissions this member already has
let permissions = BigInt(
await calculateBasePermissions(channel.guildID, member),
);
// First calculate @everyone overwrites since these have the lowest priority
const overwriteEveryone = channel?.permissionOverwrites.find(
(overwrite) => overwrite.id === (channel as Channel).guildID,
);
if (overwriteEveryone) {
// First remove denied permissions since denied < allowed
permissions &= ~BigInt(overwriteEveryone.deny);
permissions |= BigInt(overwriteEveryone.allow);
}
const overwrites = channel?.permissionOverwrites;
// In order to calculate the role permissions correctly we need to temporarily save the allowed and denied permissions
let allow = 0n;
let deny = 0n;
const memberRoles = member.guilds.get(channel.guildID)?.roles || [];
// Second calculate members role overwrites since these have middle priority
for (const overwrite of overwrites) {
if (!memberRoles.includes(overwrite.id)) continue;
deny &= ~BigInt(overwrite.deny);
allow |= BigInt(overwrite.allow);
}
// After role overwrite calculate save allowed permissions first we remove denied permissions since "denied < allowed"
permissions &= ~deny;
permissions |= allow;
// Third calculate member specific overwrites since these have the highest priority
const overwriteMember = overwrites.find(
(overwrite) => overwrite.id === (member as Member).id,
);
if (overwriteMember) {
permissions &= ~BigInt(overwriteMember.deny);
permissions |= BigInt(overwriteMember.allow);
}
return permissions.toString();
}
/** Checks if the given permission bits are matching the given permissions. `ADMINISTRATOR` always returns `true` */
export function validatePermissions(
permissionBits: string,
permissions: Permission[],
) {
if (BigInt(permissionBits) & 8n) return true;
return permissions.every(
(permission) =>
// Check if permission is in permissionBits
BigInt(permissionBits) & BigInt(Permissions[permission]),
);
}
export async function botHasPermission(
guildID: string,
/** Checks if the given member has these permissions in the given guild */
export async function hasGuildPermissions(
guild: string | Guild,
member: string | Member,
permissions: Permission[],
) {
const guild = await cacheHandlers.get("guilds", guildID);
if (!guild) return false;
// Check if the bot is the owner of the guild, if it is, returns true
if (guild.ownerID === botID) return true;
const member = await cacheHandlers.get("members", botID);
if (!member) return false;
// The everyone role is not in member.roles
const permissionBits = [...member.guilds.get(guildID)?.roles || [], guild.id]
.map((id) => guild.roles.get(id)!)
// Remove any edge case undefined
.filter((r) => r)
.reduce((bits, data) => {
bits |= BigInt(data.permissions);
return bits;
}, BigInt(0));
if (permissionBits & BigInt(Permissions.ADMINISTRATOR)) return true;
return permissions.every((permission) =>
permissionBits & BigInt(Permissions[permission])
);
// First we need the role permission bits this member has
const basePermissions = await calculateBasePermissions(guild, member);
// Second use the validatePermissions function to check if the member has every permission
return validatePermissions(basePermissions, permissions);
}
/** Checks if the bot has the permissions in a channel */
export function botHasChannelPermissions(
channelID: string,
/** Checks if the bot has these permissions in the given guild */
export function botHasGuildPermissions(
guild: string | Guild,
permissions: Permission[],
) {
return hasChannelPermissions(channelID, botID, permissions);
// Since Bot is a normal member we can use the hasRolePermissions() function
return hasGuildPermissions(guild, botID, permissions);
}
/** Checks if a user has permissions in a channel. */
/** Checks if the given member has these permissions for the given channel */
export async function hasChannelPermissions(
channelID: string,
memberID: string,
channel: string | Channel,
member: string | Member,
permissions: Permission[],
) {
const channel = await cacheHandlers.get("channels", channelID);
if (!channel) return false;
if (!channel.guildID) return true;
const guild = await cacheHandlers.get("guilds", channel.guildID);
if (!guild) return false;
if (guild.ownerID === memberID) return true;
if (
await memberIDHasPermission(memberID, guild.id, ["ADMINISTRATOR"])
) {
return true;
}
const member = (await cacheHandlers.get("members", memberID))?.guilds.get(
guild.id,
// First we need the overwrite bits this member has
const channelOverwrites = await calculateChannelOverwrites(
channel,
member,
);
if (!member) return false;
// Second use the validatePermissions function to check if the member has every permission
return validatePermissions(channelOverwrites, permissions);
}
let memberOverwrite: RawOverwrite | undefined;
let everyoneOverwrite: RawOverwrite | undefined;
const rolesOverwrites: RawOverwrite[] = [];
/** Checks if the bot has these permissions f0r the given channel */
export function botHasChannelPermissions(
channel: string | Channel,
permissions: Permission[],
) {
// Since Bot is a normal member we can use the hasRolePermissions() function
return hasChannelPermissions(channel, botID, permissions);
}
for (const overwrite of channel.permissionOverwrites || []) {
// If the overwrite on this channel is specific to this member
if (overwrite.id === memberID) memberOverwrite = overwrite;
// If it is the everyone role overwrite
if (overwrite.id === guild.id) everyoneOverwrite = overwrite;
// If it is one of the roles the member has
if (member.roles.includes(overwrite.id)) rolesOverwrites.push(overwrite);
/** Returns the permissions that are not in the given permissionBits */
export function missingPermissions(
permissionBits: string,
permissions: Permission[],
) {
if (BigInt(permissionBits) & 8n) return [];
return permissions.filter(
(permission) => !(BigInt(permissionBits) & BigInt(Permissions[permission])),
);
}
/** Get the missing Guild permissions this member has */
export async function getMissingGuildPermissions(
guild: string | Guild,
member: string | Member,
permissions: Permission[],
) {
// First we need the role permissino bits this member has
const permissionBits = await calculateBasePermissions(guild, member);
// Second returnn the members missing permissions
return missingPermissions(permissionBits, permissions);
}
/** Get the missing Channel permissions this member has */
export async function getMissingChannelPermissions(
channel: string | Channel,
member: string | Member,
permissions: Permission[],
) {
// First we need the role permissino bits this member has
const permissionBits = await calculateChannelOverwrites(channel, member);
// Second returnn the members missing permissions
return missingPermissions(permissionBits, permissions);
}
/** Throws an error if this member has not all of the given permissions */
export async function requireGuildPermissions(
guild: string | Guild,
member: string | Member,
permissions: Permission[],
) {
const missing = await getMissingGuildPermissions(guild, member, permissions);
if (missing.length) {
// If the member is missing a permission throw an Error
throw new Error(`Missing Permissions: ${missing.join(" & ")}`);
}
}
const allowedPermissions = new Set<Permission>();
/** Throws an error if the bot does not have all permissions */
export function requireBotGuildPermissions(
guild: string | Guild,
permissions: Permission[],
) {
// Since Bot is a normal member we can use the throwOnMissingGuildPermission() function
return requireGuildPermissions(guild, botID, permissions);
}
// Member perms override everything so we must check them first
if (memberOverwrite) {
const allowBits = memberOverwrite.allow;
const denyBits = memberOverwrite.deny;
for (const perm of permissions) {
// One of the necessary permissions is denied. Since this is main permission we can cancel if its denied.
if (BigInt(denyBits) & BigInt(Permissions[perm])) return false;
// Already allowed perm
if (allowedPermissions.has(perm)) continue;
// This perm is allowed so we save it
if (BigInt(allowBits) & BigInt(Permissions[perm])) {
allowedPermissions.add(perm);
}
}
/** Throws an error if this member has not all of the given permissions */
export async function requireChannelPermissions(
channel: string | Channel,
member: string | Member,
permissions: Permission[],
) {
const missing = await getMissingChannelPermissions(
channel,
member,
permissions,
);
if (missing.length) {
// If the member is missing a permission throw an Error
throw new Error(`Missing Permissions: ${missing.join(" & ")}`);
}
}
// Check the necessary permissions for roles
for (const perm of permissions) {
// If this is already allowed, skip
if (allowedPermissions.has(perm)) continue;
for (const overwrite of rolesOverwrites) {
const allowBits = overwrite.allow;
// This perm is allowed so we save it
if (BigInt(allowBits) & BigInt(Permissions[perm])) {
allowedPermissions.add(perm);
break;
}
const denyBits = overwrite.deny;
// If this role denies it we need to save and check if another role allows it, allows > deny
if (BigInt(denyBits) & BigInt(Permissions[perm])) {
// This role denies his perm, but before denying we need to check all other roles if any allow as allow > deny
const isAllowed = rolesOverwrites.some((o) =>
BigInt(o.allow) & BigInt(Permissions[perm])
);
if (isAllowed) continue;
// This permission is in fact denied. Since Roles overrule everything below here we can cancel ou here
return false;
}
}
}
if (everyoneOverwrite) {
const allowBits = everyoneOverwrite.allow;
const denyBits = everyoneOverwrite.deny;
for (const perm of permissions) {
// Already allowed perm
if (allowedPermissions.has(perm)) continue;
// One of the necessary permissions is denied. Since everyone overwrite overrides role perms we can cancel here
if (BigInt(denyBits) & BigInt(Permissions[perm])) return false;
// This perm is allowed so we save it
if (BigInt(allowBits) & BigInt(Permissions[perm])) {
allowedPermissions.add(perm);
}
}
}
// Is there any remaining permission to check role perms or can we determine that permissions are allowed
if (permissions.every((perm) => allowedPermissions.has(perm))) return true;
// Some permission was not explicitly allowed so we default to checking role perms directly
return memberIDHasPermission(memberID, guild.id, permissions);
/** Throws an error if the bot has not all of the given channel permissions */
export function requireBotChannelPermissions(
channel: string | Channel,
permissions: Permission[],
) {
// Since Bot is a normal member we can use the throwOnMissingChannelPermission() function
return requireChannelPermissions(channel, botID, permissions);
}
/** This function converts a bitwise string to permission strings */
export function calculatePermissions(permissionBits: bigint) {
return Object.keys(Permissions).filter((perm) => {
if (Number(perm)) return false;
return permissionBits & BigInt(Permissions[perm as Permission]);
return Object.keys(Permissions).filter((permission) => {
// Since Object.keys() not only returns the permission names but also the bit values we need to return false if it is a Number
if (Number(permission)) return false;
// Check if permissionBits has this permission
return permissionBits & BigInt(Permissions[permission as Permission]);
}) as Permission[];
}
/** This function converts an array of permissions into the bitwise string. */
export function calculateBits(permissions: Permission[]) {
return permissions.reduce(
(bits, perm) => bits |= BigInt(Permissions[perm]),
BigInt(0),
).toString();
return permissions
.reduce((bits, perm) => {
bits |= BigInt(Permissions[perm]);
return bits;
}, 0n)
.toString();
}
export async function highestRole(guildID: string, memberID: string) {
const guild = await cacheHandlers.get("guilds", guildID);
if (!guild) return;
/** Gets the highest role from the member in this guild */
export async function highestRole(
guild: string | Guild,
member: string | Member,
) {
guild = await getCached("guilds", guild);
const member = (await cacheHandlers.get("members", memberID))?.guilds.get(
guildID,
);
if (!member) return;
// Get the roles from the member
const memberRoles = (
await getCached("members", member)
).guilds.get(guild.id)?.roles;
// This member has no roles so the highest one is the @everyone role
if (!memberRoles) return guild.roles.get(guild.id) as Role;
let memberHighestRole: Role | undefined;
for (const roleID of member.roles) {
for (const roleID of memberRoles) {
const role = guild.roles.get(roleID);
// Rare edge case handling if undefined
if (!role) continue;
// If memberHighestRole is still undefined we want to assign the role,
// else we want to check if the current role position is higher than the current memberHighestRole
if (
!memberHighestRole || memberHighestRole.position < role.position
!memberHighestRole ||
memberHighestRole.position < role.position ||
memberHighestRole.position === role.position
) {
memberHighestRole = role;
}
}
return memberHighestRole || (guild.roles.get(guild.id) as Role);
// The member has at least one role so memberHighestRole must exist
return memberHighestRole!;
}
/** Checks if the first role is higher than the second role */
export async function higherRolePosition(
guildID: string,
guild: string | Guild,
roleID: string,
otherRoleID: string,
) {
const guild = await cacheHandlers.get("guilds", guildID);
if (!guild) return;
guild = await getCached("guilds", guild);
const role = guild.roles.get(roleID);
const otherRole = guild.roles.get(otherRoleID);
if (!role || !otherRole) return;
if (!role || !otherRole) throw new Error(Errors.ROLE_NOT_FOUND);
// Rare edge case handling
if (role.position === otherRole.position) {
@@ -252,3 +331,17 @@ export async function higherRolePosition(
return role.position > otherRole.position;
}
/** Checks if the member has a higher position than the given role */
export async function isHigherPosition(
guild: string | Guild,
memberID: string,
compareRoleID: string,
) {
guild = await getCached("guilds", guild);
if (guild.ownerID === memberID) return true;
const memberHighestRole = await highestRole(guild, memberID);
return higherRolePosition(guild.id, memberHighestRole.id, compareRoleID);
}