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

View File

@@ -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,

View File

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

View File

@@ -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<Permission> = 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<Overwrite, "id">,
) {
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<string, BannedUser>(
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);
}

View File

@@ -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<Permission> = 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<Permission> = 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),

View File

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

View File

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

View File

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

View File

@@ -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,

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