From ebbcd762cf4403963d9544353bf5d2f30a9b1fdd Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:33:52 +0000 Subject: [PATCH] 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 * Update src/util/permissions.ts Co-authored-by: Ayyan * Update src/util/permissions.ts Co-authored-by: Ayyan * Update src/util/permissions.ts Co-authored-by: Ayyan * Update src/util/permissions.ts Co-authored-by: Ayyan * Apply suggestions from code review Co-authored-by: Ayyan * BigInt() to n * Update src/util/permissions.ts Co-authored-by: Ayyan * Update src/util/permissions.ts Co-authored-by: Ayyan * 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 ecbd30e160f0ea33d3b24c35890a0589018aa78a. * 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 Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> --- src/handlers/guilds/GUILD_CREATE.ts | 4 +- src/helpers/channel.ts | 269 +++++------------ src/helpers/guild.ts | 369 ++++++++--------------- src/helpers/member.ts | 155 +++++----- src/helpers/message.ts | 129 ++------ src/helpers/webhook.ts | 103 +++---- src/types/errors.ts | 1 + src/types/permission.ts | 5 +- src/util/permissions.ts | 447 +++++++++++++++++----------- 9 files changed, 605 insertions(+), 877 deletions(-) diff --git a/src/handlers/guilds/GUILD_CREATE.ts b/src/handlers/guilds/GUILD_CREATE.ts index 302d42e48..c43ccf9db 100644 --- a/src/handlers/guilds/GUILD_CREATE.ts +++ b/src/handlers/guilds/GUILD_CREATE.ts @@ -1,9 +1,9 @@ import { eventHandlers } from "../../bot.ts"; +import { cacheHandlers } from "../../cache.ts"; +import { structures } from "../../structures/mod.ts"; import { CreateGuildPayload, DiscordPayload } from "../../types/mod.ts"; import { cache } from "../../util/cache.ts"; import { basicShards } from "../../ws/shard.ts"; -import { structures } from "../../structures/mod.ts"; -import { cacheHandlers } from "../../cache.ts"; export async function handleGuildCreate( data: DiscordPayload, diff --git a/src/helpers/channel.ts b/src/helpers/channel.ts index b8085ba0b..a1e1d855d 100644 --- a/src/helpers/channel.ts +++ b/src/helpers/channel.ts @@ -1,4 +1,6 @@ +import { cacheHandlers } from "../cache.ts"; import { RequestManager } from "../rest/request_manager.ts"; +import { structures } from "../structures/mod.ts"; import { ChannelEditOptions, ChannelTypes, @@ -20,11 +22,10 @@ import { import { endpoints } from "../util/constants.ts"; import { botHasChannelPermissions, - botHasPermission, calculateBits, + requireBotChannelPermissions, + requireBotGuildPermissions, } from "../util/permissions.ts"; -import { cacheHandlers } from "../cache.ts"; -import { structures } from "../structures/mod.ts"; /** Checks if a channel overwrite for a user id or a role id has permission in this channel */ export function channelOverwriteHasPermission( @@ -48,33 +49,15 @@ export function channelOverwriteHasPermission( } /** Fetch a single message from the server. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */ -export async function getMessage( - channelID: string, - id: string, -) { - const hasViewChannelPerm = await botHasChannelPermissions( - channelID, - ["VIEW_CHANNEL"], - ); - if ( - !hasViewChannelPerm - ) { - throw new Error(Errors.MISSING_VIEW_CHANNEL); - } +export async function getMessage(channelID: string, id: string) { + await requireBotChannelPermissions(channelID, [ + "VIEW_CHANNEL", + "READ_MESSAGE_HISTORY", + ]); - const hasReadMessageHistoryPerm = await botHasChannelPermissions( - channelID, - ["READ_MESSAGE_HISTORY"], - ); - if ( - !hasReadMessageHistoryPerm - ) { - throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); - } - - const result = await RequestManager.get( + const result = (await RequestManager.get( endpoints.CHANNEL_MESSAGE(channelID, id), - ) as MessageCreateOptions; + )) as MessageCreateOptions; return structures.createMessageStruct(result); } @@ -88,25 +71,10 @@ export async function getMessages( | GetMessagesAround | GetMessages, ) { - const hasViewChannelPerm = await botHasChannelPermissions( - channelID, - ["VIEW_CHANNEL"], - ); - if ( - !hasViewChannelPerm - ) { - throw new Error(Errors.MISSING_VIEW_CHANNEL); - } - - const hasReadMessageHistoryPerm = await botHasChannelPermissions( - channelID, - ["READ_MESSAGE_HISTORY"], - ); - if ( - !hasReadMessageHistoryPerm - ) { - throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); - } + await requireBotChannelPermissions(channelID, [ + "VIEW_CHANNEL", + "READ_MESSAGE_HISTORY", + ]); if (options?.limit && options.limit > 100) return; @@ -174,57 +142,29 @@ export async function sendMessage( if (typeof content === "string") content = { content }; const channel = await cacheHandlers.get("channels", channelID); - // If the channel is cached, we can do extra checks/safety if (channel) { if ( - ![ChannelTypes.DM, ChannelTypes.GUILD_NEWS, ChannelTypes.GUILD_TEXT] - .includes(channel.type) + ![ + ChannelTypes.DM, + ChannelTypes.GUILD_NEWS, + ChannelTypes.GUILD_TEXT, + ].includes(channel.type) ) { throw new Error(Errors.CHANNEL_NOT_TEXT_BASED); } - const hasSendMessagesPerm = await botHasChannelPermissions( - channelID, - ["SEND_MESSAGES"], - ); - if ( - !hasSendMessagesPerm - ) { - throw new Error(Errors.MISSING_SEND_MESSAGES); + const requiredPerms: Set = new Set([ + "SEND_MESSAGES", + "VIEW_CHANNEL", + ]); + + if (content.tts) requiredPerms.add("SEND_TTS_MESSAGES"); + if (content.embed) requiredPerms.add("EMBED_LINKS"); + if (content.replyMessageID || content.mentions?.repliedUser) { + requiredPerms.add("READ_MESSAGE_HISTORY"); } - const hasSendTtsMessagesPerm = await botHasChannelPermissions( - channelID, - ["SEND_TTS_MESSAGES"], - ); - if ( - content.tts && - !hasSendTtsMessagesPerm - ) { - throw new Error(Errors.MISSING_SEND_TTS_MESSAGE); - } - - const hasEmbedLinksPerm = await botHasChannelPermissions( - channelID, - ["EMBED_LINKS"], - ); - if ( - content.embed && - !hasEmbedLinksPerm - ) { - throw new Error(Errors.MISSING_EMBED_LINKS); - } - - if (content.mentions?.repliedUser) { - if ( - !(await botHasChannelPermissions( - channelID, - ["READ_MESSAGE_HISTORY"], - )) - ) { - throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); - } - } + await requireBotChannelPermissions(channelID, [...requiredPerms]); } // Use ... for content length due to unicode characters and js .length handling @@ -235,8 +175,8 @@ export async function sendMessage( if (content.mentions) { if (content.mentions.users?.length) { if (content.mentions.parse?.includes("users")) { - content.mentions.parse = content.mentions.parse.filter((p) => - p !== "users" + content.mentions.parse = content.mentions.parse.filter( + (p) => p !== "users", ); } @@ -247,8 +187,8 @@ export async function sendMessage( if (content.mentions.roles?.length) { if (content.mentions.parse?.includes("roles")) { - content.mentions.parse = content.mentions.parse.filter((p) => - p !== "roles" + content.mentions.parse = content.mentions.parse.filter( + (p) => p !== "roles", ); } @@ -258,7 +198,7 @@ export async function sendMessage( } } - const result = await RequestManager.post( + const result = (await RequestManager.post( endpoints.CHANNEL_MESSAGES(channelID), { ...content, @@ -277,9 +217,9 @@ export async function sendMessage( } : {}), }, - ) as MessageCreateOptions; + )) as MessageCreateOptions; - return structures.createMessageStruct(result as MessageCreateOptions); + return structures.createMessageStruct(result); } /** Delete messages from the channel. 2-100. Requires the MANAGE_MESSAGES permission */ @@ -288,15 +228,8 @@ export async function deleteMessages( ids: string[], reason?: string, ) { - const hasManageMessages = await botHasChannelPermissions( - channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessages - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); + if (ids.length < 2) { throw new Error(Errors.DELETE_MESSAGES_MIN); } @@ -320,15 +253,7 @@ export async function deleteMessages( /** Gets the invites for this channel. Requires MANAGE_CHANNEL */ export async function getChannelInvites(channelID: string) { - const hasManagaChannels = await botHasChannelPermissions( - channelID, - ["MANAGE_CHANNELS"], - ); - if ( - !hasManagaChannels - ) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } + await requireBotChannelPermissions(channelID, ["MANAGE_CHANNELS"]); const result = await RequestManager.get(endpoints.CHANNEL_INVITES(channelID)); @@ -340,15 +265,7 @@ export async function createInvite( channelID: string, options: CreateInviteOptions, ) { - const hasCreateInstantInvitePerm = await botHasChannelPermissions( - channelID, - ["CREATE_INSTANT_INVITE"], - ); - if ( - !hasCreateInstantInvitePerm - ) { - throw new Error(Errors.MISSING_CREATE_INSTANT_INVITE); - } + await requireBotChannelPermissions(channelID, ["CREATE_INSTANT_INVITE"]); if (options.max_age && (options.max_age > 604800 || options.max_age < 0)) { console.log( @@ -374,52 +291,33 @@ export async function createInvite( /** Returns an invite for the given code. */ export async function getInvite(inviteCode: string) { - const result = await RequestManager.get( - endpoints.INVITE(inviteCode), - ); + const result = await RequestManager.get(endpoints.INVITE(inviteCode)); return result as InvitePayload; } /** Deletes an invite for the given code. Requires `MANAGE_CHANNELS` or `MANAGE_GUILD` permission */ -export async function deleteInvite( - channelID: string, - inviteCode: string, -) { - const hasPerm = await botHasChannelPermissions(channelID, [ +export async function deleteInvite(channelID: string, inviteCode: string) { + const channel = await cacheHandlers.get("channels", channelID); + + if (!channel) throw new Error(Errors.CHANNEL_NOT_FOUND); + + const hasPerm = await botHasChannelPermissions(channel, [ "MANAGE_CHANNELS", ]); if (!hasPerm) { - const channel = await cacheHandlers.get("channels", channelID); - - const hasManageGuildPerm = await botHasPermission(channel!.guildID, [ - "MANAGE_GUILD", - ]); - - if (!hasManageGuildPerm) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } + await requireBotGuildPermissions(channel!.guildID, ["MANAGE_GUILD"]); } - const result = await RequestManager.delete( - endpoints.INVITE(inviteCode), - ); + const result = await RequestManager.delete(endpoints.INVITE(inviteCode)); return result as InvitePayload; } /** Gets the webhooks for this channel. Requires MANAGE_WEBHOOKS */ export async function getChannelWebhooks(channelID: string) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } + await requireBotChannelPermissions(channelID, ["MANAGE_WEBHOOKS"]); const result = await RequestManager.get( endpoints.CHANNEL_WEBHOOKS(channelID), @@ -461,10 +359,7 @@ function processEditChannelQueue() { const secondDetails = request.items.shift(); if (!secondDetails) return; - return editChannel( - secondDetails.channelID, - secondDetails.options, - ); + return editChannel(secondDetails.channelID, secondDetails.options); }); if (editChannelNameTopicQueue.size) { @@ -480,15 +375,7 @@ export async function editChannel( options: ChannelEditOptions, reason?: string, ) { - const hasManageChannelsPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_CHANNELS"], - ); - if ( - !hasManageChannelsPerm - ) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } + await requireBotChannelPermissions(channelID, ["MANAGE_CHANNELS"]); if (options.name || options.topic) { const request = editChannelNameTopicQueue.get(channelID); @@ -524,24 +411,19 @@ export async function editChannel( // deno-lint-ignore camelcase user_limit: options.userLimit, // deno-lint-ignore camelcase - permission_overwrites: options.overwrites?.map( - (overwrite) => { - return { - ...overwrite, - allow: calculateBits(overwrite.allow), - deny: calculateBits(overwrite.deny), - }; - }, - ), + permission_overwrites: options.overwrites?.map((overwrite) => { + return { + ...overwrite, + allow: calculateBits(overwrite.allow), + deny: calculateBits(overwrite.deny), + }; + }), }; - const result = await RequestManager.patch( - endpoints.CHANNEL_BASE(channelID), - { - ...payload, - reason, - }, - ); + const result = await RequestManager.patch(endpoints.CHANNEL_BASE(channelID), { + ...payload, + reason, + }); return result; } @@ -551,22 +433,14 @@ export async function followChannel( sourceChannelID: string, targetChannelID: string, ) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - targetChannelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } + await requireBotChannelPermissions(targetChannelID, ["MANAGE_WEBHOOKS"]); - const data = await RequestManager.post( + const data = (await RequestManager.post( endpoints.CHANNEL_FOLLOW(sourceChannelID), { webhook_channel_id: targetChannelID, }, - ) as FollowedChannelPayload; + )) as FollowedChannelPayload; return data.webhook_id; } @@ -584,11 +458,12 @@ export async function isChannelSynced(channelID: string) { if (!parentChannel) return false; return channel.permissionOverwrites?.every((overwrite) => { - const permission = parentChannel.permissionOverwrites?.find((ow) => - ow.id === overwrite.id + const permission = parentChannel.permissionOverwrites?.find( + (ow) => ow.id === overwrite.id, ); if (!permission) return false; - return !(overwrite.allow !== permission.allow || - overwrite.deny !== permission.deny); + return !( + overwrite.allow !== permission.allow || overwrite.deny !== permission.deny + ); }); } diff --git a/src/helpers/guild.ts b/src/helpers/guild.ts index a16802c3e..5cd28ac54 100644 --- a/src/helpers/guild.ts +++ b/src/helpers/guild.ts @@ -1,5 +1,7 @@ import { identifyPayload } from "../bot.ts"; +import { cacheHandlers } from "../cache.ts"; import { RequestManager } from "../rest/request_manager.ts"; +import { Guild, Member, structures } from "../structures/mod.ts"; import { AuditLogs, BannedUser, @@ -28,6 +30,7 @@ import { Intents, MemberCreatePayload, Overwrite, + Permission, PositionSwap, PruneOptions, PrunePayload, @@ -37,22 +40,23 @@ import { } from "../types/mod.ts"; import { Collection } from "../util/collection.ts"; import { endpoints } from "../util/constants.ts"; -import { botHasPermission, calculateBits } from "../util/permissions.ts"; +import { + calculateBits, + requireBotGuildPermissions, +} from "../util/permissions.ts"; import { camelKeysToSnakeCase, formatImageURL, urlToBase64, } from "../util/utils.ts"; import { requestAllMembers } from "../ws/shard_manager.ts"; -import { cacheHandlers } from "../cache.ts"; -import { Guild, Member, structures } from "../structures/mod.ts"; /** Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. This endpoint can be used only by bots in less than 10 guilds. */ export async function createGuild(options: CreateServerOptions) { - const guild = await RequestManager.post( + const guild = (await RequestManager.post( endpoints.GUILDS, options, - ) as CreateGuildPayload; + )) as CreateGuildPayload; return structures.createGuildStruct(guild, 0); } @@ -120,25 +124,29 @@ export async function createGuildChannel( name: string, options?: ChannelCreateOptions, ) { - const hasPerm = await botHasPermission( - guildID, - ["MANAGE_CHANNELS"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } + const requiredPerms: Set = new Set(["MANAGE_CHANNELS"]); - const result = (await RequestManager.post(endpoints.GUILD_CHANNELS(guildID), { - ...options, - name, - permission_overwrites: options?.permissionOverwrites?.map((perm) => ({ - ...perm, + options?.permissionOverwrites?.forEach((overwrite) => { + overwrite.allow.forEach(requiredPerms.add, requiredPerms); + overwrite.deny.forEach(requiredPerms.add, requiredPerms); + }); - allow: calculateBits(perm.allow), - deny: calculateBits(perm.deny), - })), - type: options?.type || ChannelTypes.GUILD_TEXT, - })) as ChannelCreatePayload; + await requireBotGuildPermissions(guildID, [...requiredPerms]); + + const result = (await RequestManager.post( + endpoints.GUILD_CHANNELS(guildID), + { + ...options, + name, + permission_overwrites: options?.permissionOverwrites?.map((perm) => ({ + ...perm, + + allow: calculateBits(perm.allow), + deny: calculateBits(perm.deny), + })), + type: options?.type || ChannelTypes.GUILD_TEXT, + }, + )) as ChannelCreatePayload; const channelStruct = await structures.createChannelStruct(result); await cacheHandlers.set("channels", channelStruct.id, channelStruct); @@ -152,13 +160,7 @@ export async function deleteChannel( channelID: string, reason?: string, ) { - const hasPerm = await botHasPermission( - guildID, - ["MANAGE_CHANNELS"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } + await requireBotGuildPermissions(guildID, ["MANAGE_CHANNELS"]); const guild = await cacheHandlers.get("guilds", guildID); if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); @@ -180,13 +182,13 @@ export async function deleteChannel( } /** Returns a list of guild channel objects. -* -* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your channels will be cached in your guild.** -*/ + * + * ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your channels will be cached in your guild.** + */ export async function getChannels(guildID: string, addToCache = true) { - const result = await RequestManager.get( + const result = (await RequestManager.get( endpoints.GUILD_CHANNELS(guildID), - ) as ChannelCreatePayload[]; + ) as ChannelCreatePayload[]); return Promise.all(result.map(async (res) => { const channelStruct = await structures.createChannelStruct(res, guildID); @@ -199,13 +201,13 @@ export async function getChannels(guildID: string, addToCache = true) { } /** Fetches a single channel object from the api. -* -* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your channels will be cached in your guild.** -*/ + * + * ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your channels will be cached in your guild.** + */ export async function getChannel(channelID: string, addToCache = true) { - const result = await RequestManager.get( + const result = (await RequestManager.get( endpoints.CHANNEL_BASE(channelID), - ) as ChannelCreatePayload; + )) as ChannelCreatePayload; const channelStruct = await structures.createChannelStruct( result, @@ -242,13 +244,7 @@ export async function editChannelOverwrite( overwriteID: string, options: Omit, ) { - const hasPerm = await botHasPermission( - guildID, - ["MANAGE_ROLES"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); const result = await RequestManager.put( endpoints.CHANNEL_OVERWRITE(channelID, overwriteID), @@ -268,13 +264,7 @@ export async function deleteChannelOverwrite( channelID: string, overwriteID: string, ) { - const hasPerm = await botHasPermission( - guildID, - ["MANAGE_ROLES"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); const result = await RequestManager.delete( endpoints.CHANNEL_OVERWRITE(channelID, overwriteID), @@ -284,9 +274,9 @@ export async function deleteChannelOverwrite( } /** Returns a guild member object for the specified user. -* -* ⚠️ **ADVANCED USE ONLY: Your members will be cached in your guild most likely. Only use this when you are absolutely sure the member is not cached.** -*/ + * + * ⚠️ **ADVANCED USE ONLY: Your members will be cached in your guild most likely. Only use this when you are absolutely sure the member is not cached.** + */ export async function getMember( guildID: string, id: string, @@ -295,9 +285,9 @@ export async function getMember( const guild = await cacheHandlers.get("guilds", guildID); if (!guild && !options?.force) return; - const data = await RequestManager.get( + const data = (await RequestManager.get( endpoints.GUILD_MEMBER(guildID, id), - ) as MemberCreatePayload; + )) as MemberCreatePayload; const memberStruct = await structures.createMemberStruct(data, guildID); await cacheHandlers.set("members", memberStruct.id, memberStruct); @@ -306,9 +296,9 @@ export async function getMember( } /** Returns guild member objects for the specified user by their nickname/username. -* -* ⚠️ **ADVANCED USE ONLY: Your members will be cached in your guild most likely. Only use this when you are absolutely sure the member is not cached.** -*/ + * + * ⚠️ **ADVANCED USE ONLY: Your members will be cached in your guild most likely. Only use this when you are absolutely sure the member is not cached.** + */ export async function getMembersByQuery( guildID: string, name: string, @@ -329,10 +319,7 @@ export async function createEmoji( image: string, options: CreateEmojisOptions, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_EMOJIS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_EMOJIS); - } + await requireBotGuildPermissions(guildID, ["MANAGE_EMOJIS"]); if (image && !image.startsWith("data:image/")) { image = await urlToBase64(image); @@ -353,10 +340,7 @@ export async function editEmoji( id: string, options: EditEmojisOptions, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_EMOJIS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_EMOJIS); - } + await requireBotGuildPermissions(guildID, ["MANAGE_EMOJIS"]); const result = await RequestManager.patch( endpoints.GUILD_EMOJI(guildID, id), @@ -375,10 +359,7 @@ export async function deleteEmoji( id: string, reason?: string, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_EMOJIS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_EMOJIS); - } + await requireBotGuildPermissions(guildID, ["MANAGE_EMOJIS"]); const result = await RequestManager.delete( endpoints.GUILD_EMOJI(guildID, id), @@ -399,9 +380,9 @@ export function emojiURL(id: string, animated = false) { * ⚠️ **If you need this, you are probably doing something wrong. Always use cache.guilds.get()?.emojis */ export async function getEmojis(guildID: string, addToCache = true) { - const result = await RequestManager.get( + const result = (await RequestManager.get( endpoints.GUILD_EMOJIS(guildID), - ) as Emoji[]; + )) as Emoji[]; if (addToCache) { const guild = await cacheHandlers.get("guilds", guildID); @@ -425,9 +406,9 @@ export async function getEmoji( emojiID: string, addToCache = true, ) { - const result = await RequestManager.get( + const result = (await RequestManager.get( endpoints.GUILD_EMOJI(guildID, emojiID), - ) as Emoji; + )) as Emoji; if (addToCache) { const guild = await cacheHandlers.get("guilds", guildID); @@ -449,19 +430,13 @@ export async function createRole( options: CreateRoleOptions, reason?: string, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); - const result = await RequestManager.post( - endpoints.GUILD_ROLES(guildID), - { - ...options, - permissions: calculateBits(options?.permissions || []), - reason, - }, - ); + const result = await RequestManager.post(endpoints.GUILD_ROLES(guildID), { + ...options, + permissions: calculateBits(options?.permissions || []), + reason, + }); const roleData = result as RoleData; const role = await structures.createRoleStruct(roleData); @@ -477,10 +452,7 @@ export async function editRole( id: string, options: CreateRoleOptions, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); const result = await RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), { ...options, @@ -494,10 +466,7 @@ export async function editRole( /** Delete a guild role. Requires the MANAGE_ROLES permission. */ export async function deleteRole(guildID: string, id: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); const result = await RequestManager.delete(endpoints.GUILD_ROLE(guildID, id)); @@ -505,14 +474,11 @@ export async function deleteRole(guildID: string, id: string) { } /** Returns a list of role objects for the guild. -* -* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your roles will be cached in your guild.** -*/ + * + * ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your roles will be cached in your guild.** + */ export async function getRoles(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); const result = await RequestManager.get(endpoints.GUILD_ROLES(guildID)); @@ -521,10 +487,7 @@ export async function getRoles(guildID: string) { /** Modify the positions of a set of role objects for the guild. Requires the MANAGE_ROLES permission. */ export async function swapRoles(guildID: string, rolePositons: PositionSwap) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); const result = await RequestManager.patch( endpoints.GUILD_ROLES(guildID), @@ -541,10 +504,7 @@ export async function getPruneCount(guildID: string, options?: PruneOptions) { throw new Error(Errors.PRUNE_MAX_DAYS); } - const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_KICK_MEMBERS); - } + await requireBotGuildPermissions(guildID, ["KICK_MEMBERS"]); const result = await RequestManager.get( endpoints.GUILD_PRUNE(guildID), @@ -566,10 +526,7 @@ export async function pruneMembers( if (options.days && options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); if (options.days && options.days > 30) throw new Error(Errors.PRUNE_MAX_DAYS); - const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_KICK_MEMBERS); - } + await requireBotGuildPermissions(guildID, ["KICK_MEMBERS"]); const result = await RequestManager.post( endpoints.GUILD_PRUNE(guildID), @@ -586,7 +543,7 @@ export async function pruneMembers( * Highly recommended to use this function to fetch members instead of getMember from REST. * REST: 50/s global(across all shards) rate limit with ALL requests this included * GW(this function): 120/m(PER shard) rate limit. Meaning if you have 8 shards your limit is now 960/m. -*/ + */ export function fetchMembers(guild: Guild, options?: FetchMembersOptions) { // You can request 1 member without the intent if ( @@ -612,11 +569,8 @@ export function fetchMembers(guild: Guild, options?: FetchMembersOptions) { * Highly recommended to **NOT** use this function to get members instead use fetchMembers(). * REST(this function): 50/s global(across all shards) rate limit with ALL requests this included * GW(fetchMembers): 120/m(PER shard) rate limit. Meaning if you have 8 shards your limit is 960/m. -*/ -export async function getMembers( - guildID: string, - options?: GetMemberOptions, -) { + */ +export async function getMembers(guildID: string, options?: GetMemberOptions) { if (!(identifyPayload.intents && Intents.GUILD_MEMBERS)) { throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS); } @@ -629,21 +583,24 @@ export async function getMembers( let membersLeft = options?.limit ?? guild.memberCount; let loops = 1; while ( - (options?.limit ?? guild.memberCount) > members.size && membersLeft > 0 + (options?.limit ?? guild.memberCount) > members.size && + membersLeft > 0 ) { if (options?.limit && options.limit > 1000) { console.log( `Paginating get members from REST. #${loops} / ${ - Math.ceil((options?.limit ?? 1) / 1000) + Math.ceil( + (options?.limit ?? 1) / 1000, + ) }`, ); } - const result = await RequestManager.get( + const result = (await RequestManager.get( `${endpoints.GUILD_MEMBERS(guildID)}?limit=${ membersLeft > 1000 ? 1000 : membersLeft }${options?.after ? `&after=${options.after}` : ""}`, - ) as MemberCreatePayload[]; + )) as MemberCreatePayload[]; const memberStructures = await Promise.all( result.map(async (member) => { @@ -680,10 +637,7 @@ export async function getAuditLogs( guildID: string, options: GetAuditLogsOptions, ) { - const hasPerm = await botHasPermission(guildID, ["VIEW_AUDIT_LOG"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_VIEW_AUDIT_LOG); - } + await requireBotGuildPermissions(guildID, ["VIEW_AUDIT_LOG"]); const result = await RequestManager.get(endpoints.GUILD_AUDIT_LOGS(guildID), { ...options, @@ -700,10 +654,7 @@ export async function getAuditLogs( /** Returns the guild widget object. Requires the MANAGE_GUILD permission. */ export async function getWidgetSettings(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); const result = await RequestManager.get(endpoints.GUILD_WIDGET(guildID)); @@ -716,15 +667,12 @@ export async function editWidget( enabled: boolean, channelID?: string | null, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); - const result = await RequestManager.patch( - endpoints.GUILD_WIDGET(guildID), - { enabled, channel_id: channelID }, - ); + const result = await RequestManager.patch(endpoints.GUILD_WIDGET(guildID), { + enabled, + channel_id: channelID, + }); return result; } @@ -767,10 +715,7 @@ export async function getVanityURL(guildID: string) { /** Returns a list of integrations for the guild. Requires the MANAGE_GUILD permission. */ export async function getIntegrations(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); const result = await RequestManager.get( endpoints.GUILD_INTEGRATIONS(guildID), @@ -785,10 +730,7 @@ export async function editIntegration( id: string, options: EditIntegrationOptions, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); const result = await RequestManager.patch( endpoints.GUILD_INTEGRATION(guildID, id), @@ -800,10 +742,7 @@ export async function editIntegration( /** Delete the attached integration object for the guild with this id. Requires MANAGE_GUILD permission. */ export async function deleteIntegration(guildID: string, id: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); const result = await RequestManager.delete( endpoints.GUILD_INTEGRATION(guildID, id), @@ -814,10 +753,7 @@ export async function deleteIntegration(guildID: string, id: string) { /** Sync an integration. Requires the MANAGE_GUILD permission. */ export async function syncIntegration(guildID: string, id: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); const result = await RequestManager.post( endpoints.GUILD_INTEGRATION_SYNC(guildID, id), @@ -828,14 +764,11 @@ export async function syncIntegration(guildID: string, id: string) { /** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */ export async function getBans(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_BAN_MEMBERS); - } + await requireBotGuildPermissions(guildID, ["BAN_MEMBERS"]); - const results = await RequestManager.get( + const results = (await RequestManager.get( endpoints.GUILD_BANS(guildID), - ) as BannedUser[]; + )) as BannedUser[]; return new Collection( results.map((res) => [res.user.id, res]), @@ -844,10 +777,7 @@ export async function getBans(guildID: string) { /** Returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. */ export async function getBan(guildID: string, memberID: string) { - const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_BAN_MEMBERS); - } + await requireBotGuildPermissions(guildID, ["BAN_MEMBERS"]); const result = await RequestManager.get( endpoints.GUILD_BAN(guildID, memberID), @@ -858,25 +788,19 @@ export async function getBan(guildID: string, memberID: string) { /** Ban a user from the guild and optionally delete previous messages sent by the user. Requires the BAN_MEMBERS permission. */ export async function ban(guildID: string, id: string, options: BanOptions) { - const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_BAN_MEMBERS); - } + await requireBotGuildPermissions(guildID, ["BAN_MEMBERS"]); - const result = await RequestManager.put( - endpoints.GUILD_BAN(guildID, id), - { ...options, delete_message_days: options.days }, - ); + const result = await RequestManager.put(endpoints.GUILD_BAN(guildID, id), { + ...options, + delete_message_days: options.days, + }); return result; } /** Remove the ban for a user. Requires BAN_MEMBERS permission */ export async function unban(guildID: string, id: string) { - const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_BAN_MEMBERS); - } + await requireBotGuildPermissions(guildID, ["BAN_MEMBERS"]); const result = await RequestManager.delete(endpoints.GUILD_BAN(guildID, id)); @@ -892,10 +816,7 @@ export async function getGuildPreview(guildID: string) { /** Modify a guilds settings. Requires the MANAGE_GUILD permission. */ export async function editGuild(guildID: string, options: GuildEditOptions) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); if (options.icon && !options.icon.startsWith("data:image/")) { options.icon = await urlToBase64(options.icon); @@ -919,10 +840,7 @@ export async function editGuild(guildID: string, options: GuildEditOptions) { /** Get all the invites for this guild. Requires MANAGE_GUILD permission */ export async function getInvites(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); const result = await RequestManager.get(endpoints.GUILD_INVITES(guildID)); @@ -952,13 +870,7 @@ export async function getVoiceRegions(guildID: string) { /** Returns a list of guild webhooks objects. Requires the MANAGE_WEBHOOKs permission. */ export async function getWebhooks(guildID: string) { - const hasPerm = await botHasPermission( - guildID, - ["MANAGE_WEBHOOKS"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } + await requireBotGuildPermissions(guildID, ["MANAGE_WEBHOOKS"]); const result = await RequestManager.get(endpoints.GUILD_WEBHOOKS(guildID)); @@ -967,9 +879,7 @@ export async function getWebhooks(guildID: string) { /** This function will return the raw user payload in the rare cases you need to fetch a user directly from the API. */ export async function getUser(userID: string) { - const result = await RequestManager.get( - endpoints.USER(userID), - ); + const result = await RequestManager.get(endpoints.USER(userID)); return result as UserPayload; } @@ -982,19 +892,18 @@ export async function getUser(userID: string) { * So it does not cache the guild, you must do it manually. * */ export async function getGuild(guildID: string, counts = true) { - const result = await RequestManager.get( - endpoints.GUILDS_BASE(guildID), - { with_counts: counts }, - ); + const result = await RequestManager.get(endpoints.GUILDS_BASE(guildID), { + with_counts: counts, + }); return result as UpdateGuildPayload; } /** Returns the guild template if it exists */ export async function getTemplate(templateCode: string) { - const result = await RequestManager.get( + const result = (await RequestManager.get( endpoints.GUILD_TEMPLATE(templateCode), - ) as GuildTemplate; + ) as GuildTemplate); const template = await structures.createTemplateStruct(result); return template; @@ -1004,10 +913,7 @@ export async function getTemplate(templateCode: string) { * Returns the guild template if it exists * @deprecated will get removed in v11 use `getTemplate` instead */ -export function getGuildTemplate( - guildID: string, - templateCode: string, -) { +export function getGuildTemplate(guildID: string, templateCode: string) { return getTemplate(templateCode); } @@ -1019,7 +925,7 @@ export async function createGuildFromTemplate( templateCode: string, data: CreateGuildFromTemplate, ) { - if (await cacheHandlers.size("guilds") >= 10) { + if ((await cacheHandlers.size("guilds")) >= 10) { throw new Error( "This function can only be used by bots in less than 10 guilds.", ); @@ -1042,12 +948,11 @@ export async function createGuildFromTemplate( * Requires the `MANAGE_GUILD` permission. */ export async function getGuildTemplates(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); - const templates = await RequestManager.get( + const templates = (await RequestManager.get( endpoints.GUILD_TEMPLATES(guildID), - ) as GuildTemplate[]; + )) as GuildTemplate[]; return templates.map((template) => structures.createTemplateStruct(template)); } @@ -1060,12 +965,11 @@ export async function deleteGuildTemplate( guildID: string, templateCode: string, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); - const deletedTemplate = await RequestManager.delete( + const deletedTemplate = (await RequestManager.delete( `${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`, - ) as GuildTemplate; + )) as GuildTemplate; return structures.createTemplateStruct(deletedTemplate); } @@ -1080,24 +984,20 @@ export async function createGuildTemplate( guildID: string, data: CreateGuildTemplate, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); if (data.name.length < 1 || data.name.length > 100) { throw new Error("The name can only be in between 1-100 characters."); } - if ( - data.description?.length && - data.description.length > 120 - ) { + if (data.description?.length && data.description.length > 120) { throw new Error("The description can only be in between 0-120 characters."); } - const template = await RequestManager.post( + const template = (await RequestManager.post( endpoints.GUILD_TEMPLATES(guildID), data, - ) as GuildTemplate; + )) as GuildTemplate; return structures.createTemplateStruct(template); } @@ -1107,12 +1007,11 @@ export async function createGuildTemplate( * Requires the `MANAGE_GUILD` permission. */ export async function syncGuildTemplate(guildID: string, templateCode: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); - const template = await RequestManager.put( + const template = (await RequestManager.put( `${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`, - ) as GuildTemplate; + )) as GuildTemplate; return structures.createTemplateStruct(template); } @@ -1126,24 +1025,20 @@ export async function editGuildTemplate( templateCode: string, data: EditGuildTemplate, ) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); if (data.name?.length && (data.name.length < 1 || data.name.length > 100)) { throw new Error("The name can only be in between 1-100 characters."); } - if ( - data.description?.length && - data.description.length > 120 - ) { + if (data.description?.length && data.description.length > 120) { throw new Error("The description can only be in between 0-120 characters."); } - const template = await RequestManager.patch( + const template = (await RequestManager.patch( `${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`, data, - ) as GuildTemplate; + )) as GuildTemplate; return structures.createTemplateStruct(template); } diff --git a/src/helpers/member.ts b/src/helpers/member.ts index 519be0790..6b76355d1 100644 --- a/src/helpers/member.ts +++ b/src/helpers/member.ts @@ -1,5 +1,7 @@ import { botID } from "../bot.ts"; +import { cacheHandlers } from "../cache.ts"; import { RequestManager } from "../rest/request_manager.ts"; +import { Member, structures } from "../structures/mod.ts"; import { ChannelCreatePayload, DMChannelCreatePayload, @@ -9,16 +11,16 @@ import { ImageSize, MemberCreatePayload, MessageContent, + Permission, } from "../types/mod.ts"; import { endpoints } from "../util/constants.ts"; import { - botHasPermission, - higherRolePosition, highestRole, + isHigherPosition, + requireBotChannelPermissions, + requireBotGuildPermissions, } from "../util/permissions.ts"; import { formatImageURL, urlToBase64 } from "../util/utils.ts"; -import { cacheHandlers } from "../cache.ts"; -import { Member, structures } from "../structures/mod.ts"; import { sendMessage } from "./channel.ts"; /** The users custom avatar or the default avatar if you don't have a member object. */ @@ -56,25 +58,16 @@ export async function addRole( roleID: string, reason?: string, ) { - const botsHighestRole = await highestRole(guildID, botID); - if (botsHighestRole) { - const hasHigherRolePosition = await higherRolePosition( - guildID, - botsHighestRole.id, - roleID, - ); - if ( - !hasHigherRolePosition && - (await cacheHandlers.get("guilds", guildID))?.ownerID !== botID - ) { - throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); - } + const isHigherRolePosition = await isHigherPosition( + guildID, + botID, + roleID, + ); + if (!isHigherRolePosition) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); } - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); const result = await RequestManager.put( endpoints.GUILD_MEMBER_ROLE(guildID, memberID, roleID), @@ -91,26 +84,18 @@ export async function removeRole( roleID: string, reason?: string, ) { - const botsHighestRole = await highestRole(guildID, botID); - - if (botsHighestRole) { - const hasHigherRolePosition = await higherRolePosition( - guildID, - botsHighestRole.id, - roleID, - ); - if ( - !hasHigherRolePosition && - (await cacheHandlers.get("guilds", guildID))?.ownerID !== botID - ) { - throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); - } + const isHigherRolePosition = await isHigherPosition( + guildID, + botID, + roleID, + ); + if ( + !isHigherRolePosition + ) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); } - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); const result = await RequestManager.delete( endpoints.GUILD_MEMBER_ROLE(guildID, memberID, roleID), @@ -155,10 +140,7 @@ export async function kick(guildID: string, memberID: string, reason?: string) { throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); } - const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_KICK_MEMBERS); - } + await requireBotGuildPermissions(guildID, ["KICK_MEMBERS"]); const result = await RequestManager.delete( endpoints.GUILD_MEMBER(guildID, memberID), @@ -174,56 +156,56 @@ export async function editMember( memberID: string, options: EditMemberOptions, ) { + const requiredPerms: Set = new Set(); + if (options.nick) { if (options.nick.length > 32) { throw new Error(Errors.NICKNAMES_MAX_LENGTH); } + requiredPerms.add("MANAGE_NICKNAMES"); + } - const hasManageNickPerm = await botHasPermission( - guildID, - ["MANAGE_NICKNAMES"], - ); - if (!hasManageNickPerm) { - throw new Error(Errors.MISSING_MANAGE_NICKNAMES); + if (options.roles) requiredPerms.add("MANAGE_ROLES"); + + if ( + typeof options.mute !== "undefined" || + typeof options.deaf !== "undefined" || + (typeof options.channel_id !== "undefined" || "null") + ) { + const memberVoiceState = (await cacheHandlers.get("guilds", guildID)) + ?.voiceStates.get(memberID); + + if (!memberVoiceState?.channelID) { + throw new Error(Errors.MEMBER_NOT_IN_VOICE_CHANNEL); + } + + if (typeof options.mute !== "undefined") { + requiredPerms.add("MUTE_MEMBERS"); + } + + if (typeof options.deaf !== "undefined") { + requiredPerms.add("DEAFEN_MEMBERS"); + } + + if (options.channel_id) { + const requiredVoicePerms: Set = new Set([ + "CONNECT", + "MOVE_MEMBERS", + ]); + if (memberVoiceState) { + await requireBotChannelPermissions( + memberVoiceState?.channelID, + [...requiredVoicePerms], + ); + } + await requireBotChannelPermissions( + options.channel_id, + [...requiredVoicePerms], + ); } } - const hasManageRolesPerm = await botHasPermission( - guildID, - ["MANAGE_ROLES"], - ); - if ( - options.roles && - !hasManageRolesPerm - ) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - if (options.mute) { - const hasMuteMembersPerm = await botHasPermission( - guildID, - ["MUTE_MEMBERS"], - ); - // TODO: This should check if the member is in a voice channel - if ( - !hasMuteMembersPerm - ) { - throw new Error(Errors.MISSING_MUTE_MEMBERS); - } - } - - const hasDeafenMembersPerm = await botHasPermission( - guildID, - ["DEAFEN_MEMBERS"], - ); - if ( - options.deaf && - !hasDeafenMembersPerm - ) { - throw new Error(Errors.MISSING_DEAFEN_MEMBERS); - } - - // TODO: if channel id is provided check if the bot has CONNECT and MOVE in channel and current channel + await requireBotGuildPermissions(guildID, [...requiredPerms]); const result = await RequestManager.patch( endpoints.GUILD_MEMBER(guildID, memberID), @@ -292,8 +274,7 @@ export async function editBotNickname( guildID: string, nickname: string | null, ) { - const hasPerm = await botHasPermission(guildID, ["CHANGE_NICKNAME"]); - if (!hasPerm) throw new Error(Errors.MISSING_CHANGE_NICKNAME); + await requireBotGuildPermissions(guildID, ["CHANGE_NICKNAME"]); const response = await RequestManager.patch( endpoints.USER_NICK(guildID), diff --git a/src/helpers/message.ts b/src/helpers/message.ts index d9d57e245..240f2ccd4 100644 --- a/src/helpers/message.ts +++ b/src/helpers/message.ts @@ -1,18 +1,19 @@ import { botID } from "../bot.ts"; +import { cacheHandlers } from "../cache.ts"; import { RequestManager } from "../rest/request_manager.ts"; +import { Message, structures } from "../structures/mod.ts"; import { DiscordGetReactionsParams, Errors, MessageContent, MessageCreateOptions, + Permission, UserPayload, } from "../types/mod.ts"; import { Collection } from "../util/collection.ts"; import { endpoints } from "../util/constants.ts"; -import { botHasChannelPermissions } from "../util/permissions.ts"; +import { requireBotChannelPermissions } from "../util/permissions.ts"; import { delay } from "../util/utils.ts"; -import { cacheHandlers } from "../cache.ts"; -import { Message, structures } from "../structures/mod.ts"; /** Delete a message with the channel id and message id only. */ export async function deleteMessageByID( @@ -42,15 +43,7 @@ export async function deleteMessage( ) { if (message.author.id !== botID) { // This needs to check the channels permission not the guild permission - const hasManageMessages = await botHasChannelPermissions( - message.channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessages - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } + await requireBotChannelPermissions(message.channelID, ["MANAGE_MESSAGES"]); } if (delayMilliseconds) await delay(delayMilliseconds); @@ -65,15 +58,7 @@ export async function deleteMessage( /** Pin a message in a channel. Requires MANAGE_MESSAGES. Max pins allowed in a channel = 50. */ export async function pin(channelID: string, messageID: string) { - const hasManageMessagesPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessagesPerm - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); const result = await RequestManager.put( endpoints.CHANNEL_PIN(channelID, messageID), @@ -84,15 +69,7 @@ export async function pin(channelID: string, messageID: string) { /** Unpin a message in a channel. Requires MANAGE_MESSAGES. */ export async function unpin(channelID: string, messageID: string) { - const hasManageMessagesPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessagesPerm - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); const result = await RequestManager.delete( endpoints.CHANNEL_PIN(channelID, messageID), @@ -107,23 +84,10 @@ export async function addReaction( messageID: string, reaction: string, ) { - const hasAddReactionsPerm = await botHasChannelPermissions( - channelID, - ["ADD_REACTIONS"], - ); - if (!hasAddReactionsPerm) { - throw new Error(Errors.MISSING_ADD_REACTIONS); - } - - const hasReadMessageHistoryPerm = await botHasChannelPermissions( - channelID, - ["READ_MESSAGE_HISTORY"], - ); - if ( - !hasReadMessageHistoryPerm - ) { - throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); - } + await requireBotChannelPermissions(channelID, [ + "ADD_REACTIONS", + "READ_MESSAGE_HISTORY", + ]); if (reaction.startsWith("<:")) { reaction = reaction.substring(2, reaction.length - 1); @@ -132,11 +96,7 @@ export async function addReaction( } const result = await RequestManager.put( - endpoints.CHANNEL_MESSAGE_REACTION_ME( - channelID, - messageID, - reaction, - ), + endpoints.CHANNEL_MESSAGE_REACTION_ME(channelID, messageID, reaction), ); return result; @@ -174,11 +134,7 @@ export async function removeReaction( } const result = await RequestManager.delete( - endpoints.CHANNEL_MESSAGE_REACTION_ME( - channelID, - messageID, - reaction, - ), + endpoints.CHANNEL_MESSAGE_REACTION_ME(channelID, messageID, reaction), ); return result; @@ -191,13 +147,7 @@ export async function removeUserReaction( reaction: string, userID: string, ) { - const hasManageMessagesPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_MESSAGES"], - ); - if (!hasManageMessagesPerm) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); if (reaction.startsWith("<:")) { reaction = reaction.substring(2, reaction.length - 1); @@ -219,15 +169,7 @@ export async function removeUserReaction( /** Removes all reactions for all emojis on this message. */ export async function removeAllReactions(channelID: string, messageID: string) { - const hasManageMessagesPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessagesPerm - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); const result = await RequestManager.delete( endpoints.CHANNEL_MESSAGE_REACTIONS(channelID, messageID), @@ -242,15 +184,7 @@ export async function removeReactionEmoji( messageID: string, reaction: string, ) { - const hasManageMessagesPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessagesPerm - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); if (reaction.startsWith("<:")) { reaction = reaction.substring(2, reaction.length - 1); @@ -285,34 +219,17 @@ export async function editMessage( message: Message, content: string | MessageContent, ) { - if ( - message.author.id !== botID - ) { + if (message.author.id !== botID) { throw "You can only edit a message that was sent by the bot."; } if (typeof content === "string") content = { content }; - const hasSendMessagesPerm = await botHasChannelPermissions( - message.channelID, - ["SEND_MESSAGES"], - ); - if ( - !hasSendMessagesPerm - ) { - throw new Error(Errors.MISSING_SEND_MESSAGES); - } + const requiredPerms: Permission[] = ["SEND_MESSAGES"]; - const hasSendTtsMessagesPerm = await botHasChannelPermissions( - message.channelID, - ["SEND_TTS_MESSAGES"], - ); - if ( - content.tts && - !hasSendTtsMessagesPerm - ) { - throw new Error(Errors.MISSING_SEND_TTS_MESSAGE); - } + if (content.tts) requiredPerms.push("SEND_TTS_MESSAGES"); + + await requireBotChannelPermissions(message.channelID, requiredPerms); if (content.content && content.content.length > 2000) { throw new Error(Errors.MESSAGE_MAX_LENGTH); @@ -328,9 +245,9 @@ export async function editMessage( /** Crosspost a message in a News Channel to following channels. */ export async function publishMessage(channelID: string, messageID: string) { - const data = await RequestManager.post( + const data = (await RequestManager.post( endpoints.CHANNEL_MESSAGE_CROSSPOST(channelID, messageID), - ) as MessageCreateOptions; + )) as MessageCreateOptions; return structures.createMessageStruct(data); } diff --git a/src/helpers/webhook.ts b/src/helpers/webhook.ts index c5da8a565..87ba10926 100644 --- a/src/helpers/webhook.ts +++ b/src/helpers/webhook.ts @@ -1,5 +1,6 @@ import { applicationID } from "../bot.ts"; import { RequestManager } from "../rest/request_manager.ts"; +import { structures } from "../structures/mod.ts"; import { CreateSlashCommandOptions, EditSlashCommandOptions, @@ -22,11 +23,10 @@ import { import { cache } from "../util/cache.ts"; import { Collection } from "../util/collection.ts"; import { endpoints, SLASH_COMMANDS_NAME_REGEX } from "../util/constants.ts"; -import { botHasChannelPermissions } from "../util/permissions.ts"; +import { requireBotChannelPermissions } from "../util/permissions.ts"; import { urlToBase64 } from "../util/utils.ts"; -import { structures } from "../structures/mod.ts"; -/** +/** * Create a new webhook. Requires the MANAGE_WEBHOOKS permission. Returns a webhook object on success. Webhook names follow our naming restrictions that can be found in our Usernames and Nicknames documentation, with the following additional stipulations: * * Webhook names cannot be: 'clyde' @@ -35,21 +35,14 @@ export async function createWebhook( channelID: string, options: WebhookCreateOptions, ) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } + await requireBotChannelPermissions(channelID, ["MANAGE_WEBHOOKS"]); if ( // Specific usernames that discord does not allow options.name === "clyde" || // Character limit checks. [...] checks are because of js unicode length handling - [...options.name].length < 2 || [...options.name].length > 32 + [...options.name].length < 2 || + [...options.name].length > 32 ) { throw new Error(Errors.INVALID_WEBHOOK_NAME); } @@ -71,15 +64,7 @@ export async function editWebhook( webhookID: string, options: WebhookEditOptions, ) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } + await requireBotChannelPermissions(channelID, ["MANAGE_WEBHOOKS"]); const result = await RequestManager.patch(endpoints.WEBHOOK_ID(webhookID), { ...options, @@ -105,15 +90,7 @@ export async function editWebhookWithToken( /** Delete a webhook permanently. Requires the `MANAGE_WEBHOOKS` permission. Returns a undefined on success */ export async function deleteWebhook(channelID: string, webhookID: string) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } + await requireBotChannelPermissions(channelID, ["MANAGE_WEBHOOKS"]); const result = await RequestManager.delete(endpoints.WEBHOOK_ID(webhookID)); @@ -141,9 +118,7 @@ export async function getWebhook(webhookID: string) { /** Returns the new webhook object for the given id, this call does not require authentication and returns no user in the webhook object. */ export async function getWebhookWithToken(webhookID: string, token: string) { - const result = await RequestManager.get( - endpoints.WEBHOOK(webhookID, token), - ); + const result = await RequestManager.get(endpoints.WEBHOOK(webhookID, token)); return result as WebhookPayload; } @@ -169,8 +144,8 @@ export async function executeWebhook( if (options.mentions) { if (options.mentions.users?.length) { if (options.mentions.parse.includes("users")) { - options.mentions.parse = options.mentions.parse.filter((p) => - p !== "users" + options.mentions.parse = options.mentions.parse.filter( + (p) => p !== "users", ); } @@ -181,8 +156,8 @@ export async function executeWebhook( if (options.mentions.roles?.length) { if (options.mentions.parse.includes("roles")) { - options.mentions.parse = options.mentions.parse.filter((p) => - p !== "roles" + options.mentions.parse = options.mentions.parse.filter( + (p) => p !== "roles", ); } @@ -224,9 +199,9 @@ export async function editWebhookMessage( if (options.allowed_mentions) { if (options.allowed_mentions.users?.length) { if (options.allowed_mentions.parse.includes("users")) { - options.allowed_mentions.parse = options.allowed_mentions.parse.filter(( - p, - ) => p !== "users"); + options.allowed_mentions.parse = options.allowed_mentions.parse.filter( + (p) => p !== "users", + ); } if (options.allowed_mentions.users.length > 100) { @@ -239,9 +214,9 @@ export async function editWebhookMessage( if (options.allowed_mentions.roles?.length) { if (options.allowed_mentions.parse.includes("roles")) { - options.allowed_mentions.parse = options.allowed_mentions.parse.filter(( - p, - ) => p !== "roles"); + options.allowed_mentions.parse = options.allowed_mentions.parse.filter( + (p) => p !== "roles", + ); } if (options.allowed_mentions.roles.length > 100) { @@ -410,11 +385,7 @@ export async function upsertSlashCommand( const result = await RequestManager.patch( guildID - ? endpoints.COMMANDS_GUILD_ID( - applicationID, - guildID, - commandID, - ) + ? endpoints.COMMANDS_GUILD_ID(applicationID, guildID, commandID) : endpoints.COMMANDS_ID(applicationID, commandID), options, ); @@ -424,7 +395,7 @@ export async function upsertSlashCommand( /** * Bulk edit existing slash commands. If a command does not exist, it will create it. - * + * * **NOTE:** Any slash commands that are not specified in this function will be **deleted**. If you don't provide the commandID and rename your command, the command gets a new ID. */ export async function upsertSlashCommands( @@ -444,8 +415,8 @@ export async function upsertSlashCommands( } // TODO: remove this function for v11 -/** - * Edit an existing slash command. +/** + * Edit an existing slash command. * @deprecated This function will be removed in v11. Use `upsertSlashCommand()` instead */ export async function editSlashCommand( @@ -458,18 +429,15 @@ export async function editSlashCommand( } if ( - [...options.description].length < 1 || [...options.description].length > 100 + [...options.description].length < 1 || + [...options.description].length > 100 ) { throw new Error(Errors.INVALID_SLASH_DESCRIPTION); } const result = await RequestManager.patch( guildID - ? endpoints.COMMANDS_GUILD_ID( - applicationID, - guildID, - commandID, - ) + ? endpoints.COMMANDS_GUILD_ID(applicationID, guildID, commandID) : endpoints.COMMANDS_ID(applicationID, commandID), options, ); @@ -518,7 +486,7 @@ export async function executeSlashCommand( } // If no mentions are provided, force disable mentions - if (!(options.data.allowed_mentions)) { + if (!options.data.allowed_mentions) { options.data.allowed_mentions = { parse: [] }; } @@ -531,10 +499,7 @@ export async function executeSlashCommand( } /** To delete your response to a slash command. If a message id is not provided, it will default to deleting the original response. */ -export async function deleteSlashResponse( - token: string, - messageID?: string, -) { +export async function deleteSlashResponse(token: string, messageID?: string) { const result = await RequestManager.delete( messageID ? endpoints.INTERACTION_ID_TOKEN_MESSAGEID( @@ -564,9 +529,9 @@ export async function editSlashResponse( if (options.allowed_mentions) { if (options.allowed_mentions.users?.length) { if (options.allowed_mentions.parse.includes("users")) { - options.allowed_mentions.parse = options.allowed_mentions.parse.filter(( - p, - ) => p !== "users"); + options.allowed_mentions.parse = options.allowed_mentions.parse.filter( + (p) => p !== "users", + ); } if (options.allowed_mentions.users.length > 100) { @@ -579,9 +544,9 @@ export async function editSlashResponse( if (options.allowed_mentions.roles?.length) { if (options.allowed_mentions.parse.includes("roles")) { - options.allowed_mentions.parse = options.allowed_mentions.parse.filter(( - p, - ) => p !== "roles"); + options.allowed_mentions.parse = options.allowed_mentions.parse.filter( + (p) => p !== "roles", + ); } if (options.allowed_mentions.roles.length > 100) { diff --git a/src/types/errors.ts b/src/types/errors.ts index fb719c3c6..fa1dd616c 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -13,6 +13,7 @@ export enum Errors { GUILD_WIDGET_NOT_ENABLED = "GUILD_WIDGET_NOT_ENABLED", GUILD_NOT_FOUND = "GUILD_NOT_FOUND", MEMBER_NOT_FOUND = "MEMBER_NOT_FOUND", + MEMBER_NOT_IN_VOICE_CHANNEL = "MEMBER_NOT_IN_VOICE_CHANNEL", PRUNE_MAX_DAYS = "PRUNE_MAX_DAYS", ROLE_NOT_FOUND = "ROLE_NOT_FOUND", // Message Delete Errors diff --git a/src/types/permission.ts b/src/types/permission.ts index cd8006cb2..06435c0fe 100644 --- a/src/types/permission.ts +++ b/src/types/permission.ts @@ -9,6 +9,8 @@ export enum Permissions { MANAGE_GUILD = 0x00000020, ADD_REACTIONS = 0x00000040, VIEW_AUDIT_LOG = 0x00000080, + PRIORITY_SPEAKER = 0x00000100, + STREAM = 0x00000200, VIEW_CHANNEL = 0x00000400, SEND_MESSAGES = 0x00000800, SEND_TTS_MESSAGES = 0x00001000, @@ -18,14 +20,13 @@ export enum Permissions { READ_MESSAGE_HISTORY = 0x00010000, MENTION_EVERYONE = 0x00020000, USE_EXTERNAL_EMOJIS = 0x00040000, + VIEW_GUILD_INSIGHTS = 0x00080000, CONNECT = 0x00100000, SPEAK = 0x00200000, MUTE_MEMBERS = 0x00400000, DEAFEN_MEMBERS = 0x00800000, MOVE_MEMBERS = 0x01000000, USE_VAD = 0x02000000, - PRIORITY_SPEAKER = 0x00000100, - STREAM = 0x00000200, CHANGE_NICKNAME = 0x04000000, MANAGE_NICKNAMES = 0x08000000, MANAGE_ROLES = 0x10000000, diff --git a/src/util/permissions.ts b/src/util/permissions.ts index 05ae3d8a2..390e93b72 100644 --- a/src/util/permissions.ts +++ b/src/util/permissions.ts @@ -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; +async function getCached( + table: "channels", + key: string | Channel, +): Promise; +async function getCached( + table: "members", + key: string | Member, +): Promise; +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(); +/** 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); +}