Merge branch 'discordeno:main' into 1898

This commit is contained in:
lts20050703
2022-01-28 15:52:12 +07:00
committed by GitHub
89 changed files with 4124 additions and 0 deletions

21
plugins/cache/README.md vendored Normal file
View File

@@ -0,0 +1,21 @@
# cache-plugin
This is an official plugin maintained by Discordeno. This plugin provides
automatic caching. Remember Discordeno does not cache by default. This plugin is
NOT recommended for big bot developers but this is useful for smaller bots who
just want simple functionality.
## Usage
```ts
// MOVE TO DEPS.TS AND USE SPECIFIC VERSION
import { enableCachePlugin, enableCacheSweepers } from "https://deno.land/x/discordeno_cache_plugin/mod.ts";
// Create the bot object, THIS WILL NEED YOUR OPTIONS.
const baseBot = createBot({});
// Enables the cache plugin on this bot
const bot = enableCachePlugin(baseBot);
enableCacheSweepers(bot);
// Start your bot
await startBot(bot);
```

1
plugins/cache/deps.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from "../../mod.ts";

159
plugins/cache/mod.ts vendored Normal file
View File

@@ -0,0 +1,159 @@
import {
Bot,
Collection,
GuildEmojisUpdate,
SnakeCasedPropertiesDeep,
} from "./deps.ts";
import { setupCacheRemovals } from "./src/setupCacheRemovals.ts";
import {
addCacheCollections,
BotWithCache,
} from "./src/addCacheCollections.ts";
import { setupCacheEdits } from "./src/setupCacheEdits.ts";
// PLUGINS MUST TAKE A BOT ARGUMENT WHICH WILL BE MODIFIED
export function enableCachePlugin<B extends Bot = Bot>(rawBot: B): BotWithCache<B> {
// MARK THIS PLUGIN BEING USED
rawBot.enabledPlugins.add("CACHE");
// CUSTOMIZATION GOES HERE
const bot = addCacheCollections(rawBot);
// Get the unmodified transformer.
const { guild, user, member, channel, message, presence, role } =
bot.transformers;
// Override the transformer
bot.transformers.guild = function (_, payload) {
// Run the unmodified transformer
const result = guild(bot, payload);
// Cache the result
if (result) {
bot.guilds.set(result.id, result);
const channels = payload.guild.channels || [];
channels.forEach((channel) => {
bot.transformers.channel(bot, { channel, guildId: result.id });
});
}
// Return the result
return result;
};
// Override the transformer
bot.transformers.user = function (...args) {
// Run the unmodified transformer
const result = user(...args);
// Cache the result
if (result) {
bot.users.set(result.id, result);
}
// Return the result
return result;
};
// Override the transformer
bot.transformers.member = function (...args) {
// Run the unmodified transformer
const result = member(...args);
// Cache the result
if (result) {
bot.members.set(
bot.transformers.snowflake(`${result.id}${result.guildId}`),
result,
);
}
// Return the result
return result;
};
// Override the transformer
bot.transformers.channel = function (...args) {
// Run the unmodified transformer
const result = channel(...args);
// Cache the result
if (result) {
bot.channels.set(result.id, result);
}
// Return the result
return result;
};
// Override the transformer
bot.transformers.message = function (_, payload) {
// Run the unmodified transformer
const result = message(bot, payload);
// Cache the result
if (result) {
bot.messages.set(result.id, result);
// CACHE THE USER
const user = bot.transformers.user(bot, payload.author);
bot.users.set(user.id, user);
if (payload.guild_id && payload.member) {
const guildId = bot.transformers.snowflake(payload.guild_id);
// CACHE THE MEMBER
bot.members.set(
bot.transformers.snowflake(`${payload.author.id}${payload.guild_id}`),
bot.transformers.member(bot, payload.member, guildId, user.id),
);
}
}
// Return the result
return result;
};
// Override the transformer
bot.transformers.presence = function (...args) {
// Run the unmodified transformer
const result = presence(...args);
// Cache the result
if (result) {
bot.presences.set(result.user.id, result);
}
// Return the result
return result;
};
// Override the transformer
bot.transformers.role = function (...args) {
// Run the unmodified transformer
const result = role(...args);
// Cache the result
if (result) {
bot.guilds.get(result.guildId)?.roles.set(result.id, result);
}
// Return the result
return result;
};
const { GUILD_EMOJIS_UPDATE } = bot.handlers;
bot.handlers.GUILD_EMOJIS_UPDATE = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<GuildEmojisUpdate>;
const guild = bot.guilds.get(bot.transformers.snowflake(payload.guild_id));
if (guild) {
guild.emojis = new Collection(payload.emojis.map((e) => {
const emoji = bot.transformers.emoji(bot, e);
return [emoji.id!, emoji];
}));
}
GUILD_EMOJIS_UPDATE(bot, data, shardId);
};
setupCacheRemovals(bot);
setupCacheEdits(bot);
// PLUGINS MUST RETURN THE BOT
return bot;
}
export default enableCachePlugin;
export * from "./src/addCacheCollections.ts";
export * from "./src/dispatchRequirements.ts";
export * from "./src/setupCacheEdits.ts";
export * from "./src/setupCacheRemovals.ts";
export * from "./src/sweepers.ts";

View File

@@ -0,0 +1,39 @@
import {
Bot,
Collection,
DiscordenoChannel,
DiscordenoGuild,
DiscordenoMember,
DiscordenoMessage,
DiscordenoPresence,
DiscordenoUser,
} from "../deps.ts";
export type BotWithCache<B extends Bot = Bot> = B & CacheProps;
export interface CacheProps extends Bot {
guilds: Collection<bigint, DiscordenoGuild>;
users: Collection<bigint, DiscordenoUser>;
members: Collection<bigint, DiscordenoMember>;
channels: Collection<bigint, DiscordenoChannel>;
messages: Collection<bigint, DiscordenoMessage>;
presences: Collection<bigint, DiscordenoPresence>;
dispatchedGuildIds: Set<bigint>;
dispatchedChannelIds: Set<bigint>;
activeGuildIds: Set<bigint>;
}
export function addCacheCollections<B extends Bot>(bot: B): BotWithCache<B> {
const cacheBot = bot as BotWithCache<B>;
cacheBot.guilds = new Collection();
cacheBot.users = new Collection();
cacheBot.members = new Collection();
cacheBot.channels = new Collection();
cacheBot.messages = new Collection();
cacheBot.presences = new Collection();
cacheBot.dispatchedGuildIds = new Set();
cacheBot.dispatchedChannelIds = new Set();
cacheBot.activeGuildIds = new Set();
return bot as BotWithCache<B>;
}

View File

@@ -0,0 +1,99 @@
import { Bot, GatewayPayload } from "../deps.ts";
import { BotWithCache } from "./addCacheCollections.ts";
const processing = new Set<bigint>();
export async function dispatchRequirements<B extends Bot>(
bot: BotWithCache<B>,
data: GatewayPayload,
) {
// DELETE MEANS WE DONT NEED TO FETCH. CREATE SHOULD HAVE DATA TO CACHE
if (data.t && ["GUILD_CREATE", "GUILD_DELETE"].includes(data.t)) return;
const id = bot.utils.snowflakeToBigint(
(data.t && ["GUILD_UPDATE"].includes(data.t)
? // deno-lint-ignore no-explicit-any
(data.d as any)?.id
: // deno-lint-ignore no-explicit-any
(data.d as any)?.guild_id) ?? "",
);
if (!id || bot.activeGuildIds.has(id)) return;
// If this guild is in cache, it has not been swept and we can cancel
if (bot.guilds.has(id)) {
bot.activeGuildIds.add(id);
return;
}
if (processing.has(id)) {
bot.events.debug(
`[DISPATCH] New Guild ID already being processed: ${id} in ${data.t} event`,
);
let runs = 0;
do {
await bot.utils.delay(500);
runs++;
} while (processing.has(id) && runs < 40);
if (!processing.has(id)) return;
return bot.events.debug(
`[DISPATCH] Already processed guild was not successfully fetched: ${id} in ${data.t} event`,
);
}
processing.add(id);
// New guild id has appeared, fetch all relevant data
bot.events.debug(
`[DISPATCH] New Guild ID has appeared: ${id} in ${data.t} event`,
);
const guild = (await bot.helpers
.getGuild(id, {
counts: true,
})
.catch(console.log));
if (!guild) {
processing.delete(id);
return bot.events.debug(`[DISPATCH] Guild ID ${id} failed to fetch.`);
}
bot.events.debug(`[DISPATCH] Guild ID ${id} has been found. ${guild.name}`);
const [channels, botMember] = await Promise.all([
bot.helpers.getChannels(id),
bot.helpers.getMember(id, bot.id),
]).catch((error) => {
bot.events.debug(error);
return [];
});
if (!botMember || !channels) {
processing.delete(id);
return bot.events.debug(
`[DISPATCH] Guild ID ${id} Name: ${guild.name} failed. Unable to get botMember or channels`,
);
}
// Add to cache
bot.guilds.set(id, guild);
bot.dispatchedGuildIds.delete(id);
channels.forEach((channel) => {
bot.dispatchedChannelIds.delete(channel.id);
bot.channels.set(channel.id, channel);
});
bot.members.set(
bot.transformers.snowflake(`${botMember.id}${guild.id}`),
botMember,
);
processing.delete(id);
bot.events.debug(
`[DISPATCH] Guild ID ${id} Name: ${guild.name} completely loaded.`,
);
}

120
plugins/cache/src/setupCacheEdits.ts vendored Normal file
View File

@@ -0,0 +1,120 @@
import type {
Bot,
GuildMemberAdd,
GuildMemberRemove,
MessageReactionAdd,
MessageReactionRemove,
MessageReactionRemoveAll,
SnakeCasedPropertiesDeep,
} from "../deps.ts";
import type { BotWithCache } from "./addCacheCollections.ts";
export function setupCacheEdits<B extends Bot>(bot: BotWithCache<B>) {
const {
GUILD_MEMBER_ADD,
GUILD_MEMBER_REMOVE,
MESSAGE_REACTION_ADD,
MESSAGE_REACTION_REMOVE,
MESSAGE_REACTION_REMOVE_ALL,
} = bot.handlers;
bot.handlers.GUILD_MEMBER_ADD = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<GuildMemberAdd>;
const guild = bot.guilds.get(bot.transformers.snowflake(payload.guild_id));
if (guild) guild.memberCount++;
GUILD_MEMBER_ADD(bot, data, shardId);
};
bot.handlers.GUILD_MEMBER_REMOVE = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<GuildMemberRemove>;
const guild = bot.guilds.get(bot.transformers.snowflake(payload.guild_id));
if (guild) guild.memberCount--;
GUILD_MEMBER_REMOVE(bot, data, shardId);
};
bot.handlers.MESSAGE_REACTION_ADD = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<MessageReactionAdd>;
const messageId = bot.transformers.snowflake(payload.message_id)
const message = bot.messages.get(messageId);
const emoji = bot.transformers.emoji(bot, payload.emoji);
// if the message is cached
if (message) {
const reactions = message.reactions?.map((r) => r.emoji.name);
const toSet = {
count: 1,
me: bot.transformers.snowflake(payload.user_id) === bot.id,
emoji: emoji,
};
// if theres no reaction add it
if (!message.reactions || !reactions) {
message.reactions = [toSet];
} else if (!reactions.includes(emoji.name)) {
message.reactions?.push(toSet);
} else { // otherwise the reaction has already been added so +1 to the reaction count
const current = message.reactions?.[reactions.indexOf(emoji.name)];
// rewrite
if (current && message.reactions?.[message.reactions.indexOf(current)]) {
message.reactions[message.reactions.indexOf(current)].count++;
}
}
}
MESSAGE_REACTION_ADD(bot, data, shardId);
}
bot.handlers.MESSAGE_REACTION_REMOVE = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<MessageReactionRemove>;
const messageId = bot.transformers.snowflake(payload.message_id)
const message = bot.messages.get(messageId);
const emoji = bot.transformers.emoji(bot, payload.emoji);
// if the message is cached
if (message) {
const reactions = message.reactions?.map((r) => r.emoji.name);
if (reactions?.indexOf(emoji.name) !== undefined) {
const current = message.reactions?.[reactions.indexOf(emoji.name)];
if (current) {
if (current.count > 0) {
current.count--;
}
// delete when count is 0
if (current.count === 0) {
message.reactions?.splice(reactions?.indexOf(emoji.name), 1);
}
// when someone deleted a reaction that doesn't exist in the cache just pass
}
}
}
MESSAGE_REACTION_REMOVE(bot, data, shardId);
}
bot.handlers.MESSAGE_REACTION_REMOVE_ALL = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<MessageReactionRemoveAll>;
const messageId = bot.transformers.snowflake(payload.message_id);
const message = bot.messages.get(messageId);
if (message) {
// when an admin deleted all the reactions of a message
message.reactions = undefined;
}
MESSAGE_REACTION_REMOVE_ALL(bot, data, shardId);
}
}

124
plugins/cache/src/setupCacheRemovals.ts vendored Normal file
View File

@@ -0,0 +1,124 @@
import {
Bot,
Channel,
Collection,
GuildBanAddRemove,
GuildEmojisUpdate,
GuildMemberRemove,
GuildRoleDelete,
MessageDelete,
MessageDeleteBulk,
} from "../deps.ts";
import { SnakeCasedPropertiesDeep, UnavailableGuild } from "../deps.ts";
import { BotWithCache } from "./addCacheCollections.ts";
export function setupCacheRemovals<B extends Bot>(bot: BotWithCache<B>) {
const {
CHANNEL_DELETE,
GUILD_BAN_ADD,
GUILD_DELETE,
GUILD_EMOJIS_UPDATE,
GUILD_MEMBER_REMOVE,
GUILD_ROLE_DELETE,
MESSAGE_DELETE_BULK,
} = bot.handlers;
bot.handlers.GUILD_DELETE = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<UnavailableGuild>;
const id = bot.transformers.snowflake(payload.id);
bot.guilds.delete(id);
bot.channels.forEach((channel) => {
if (channel.guildId === id) bot.channels.delete(channel.id);
});
bot.members.forEach((member) => {
if (member.guildId === id) bot.members.delete(member.id);
});
bot.messages.forEach((message) => {
if (message.guildId === id) bot.messages.delete(message.id);
});
GUILD_DELETE(bot, data, shardId);
};
bot.handlers.CHANNEL_DELETE = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<Channel>;
// HANDLER BEFORE DELETING, BECAUSE HANDLER RUNS TRANSFORMER WHICH RECACHES
CHANNEL_DELETE(bot, data, shardId);
const id = bot.transformers.snowflake(payload.id);
bot.channels.delete(id);
bot.messages.forEach((message) => {
if (message.channelId === id) bot.messages.delete(message.id);
});
};
bot.handlers.GUILD_MEMBER_REMOVE = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<GuildMemberRemove>;
bot.members.delete(bot.transformers.snowflake(payload.user.id));
GUILD_MEMBER_REMOVE(bot, data, shardId);
};
bot.handlers.GUILD_BAN_ADD = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<GuildBanAddRemove>;
bot.members.delete(bot.transformers.snowflake(payload.user.id));
GUILD_BAN_ADD(bot, data, shardId);
};
bot.handlers.GUILD_EMOJIS_UPDATE = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<GuildEmojisUpdate>;
const guild = bot.guilds.get(bot.transformers.snowflake(payload.guild_id));
if (guild) {
guild.emojis = new Collection(payload.emojis.map((e) => {
const emoji = bot.transformers.emoji(bot, e);
return [emoji.id!, emoji];
}));
}
GUILD_EMOJIS_UPDATE(bot, data, shardId);
};
bot.handlers.MESSAGE_DELETE = function (_, data) {
const payload = data.d as SnakeCasedPropertiesDeep<MessageDelete>;
const id = bot.transformers.snowflake(payload.id);
const message = bot.messages.get(id);
bot.events.messageDelete(bot, {
id,
channelId: bot.transformers.snowflake(payload.channel_id),
guildId: payload.guild_id
? bot.transformers.snowflake(payload.guild_id)
: undefined,
}, message);
bot.messages.delete(id);
};
bot.handlers.MESSAGE_DELETE_BULK = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<MessageDeleteBulk>;
payload.ids.forEach((id) =>
bot.messages.delete(bot.transformers.snowflake(id))
);
MESSAGE_DELETE_BULK(bot, data, shardId);
};
bot.handlers.GUILD_ROLE_DELETE = function (_, data, shardId) {
const payload = data.d as SnakeCasedPropertiesDeep<GuildRoleDelete>;
const guild = bot.guilds.get(
bot.transformers.snowflake(payload.guild_id),
);
const id = bot.transformers.snowflake(payload.role_id);
if (guild) {
guild.roles.delete(id);
bot.members.forEach((member) => {
// SKIP MEMBERS IN OTHER GUILDS
if (member.guildId !== guild.id) return;
// SKIP MEMBERS WHO DON'T HAVE ROLE
if (!member.roles.includes(id)) return;
// EDIT THE MEMBERS ROLES
member.roles = member.roles.filter((roleId) => roleId !== id);
});
}
GUILD_ROLE_DELETE(bot, data, shardId);
};
}

76
plugins/cache/src/sweepers.ts vendored Normal file
View File

@@ -0,0 +1,76 @@
import { Bot } from "../deps.ts";
import { BotWithCache } from "./addCacheCollections.ts";
import { dispatchRequirements } from "./dispatchRequirements.ts";
/** Enables sweepers for your bot but will require, enabling cache first. */
export function enableCacheSweepers<B extends Bot>(bot: BotWithCache<B>) {
bot.guilds.startSweeper({
filter: function (guild, _, bot: BotWithCache<B>) {
// Reset activity for next interval
if (bot.activeGuildIds.delete(guild.id)) return false;
// This is inactive guild. Not a single thing has happened for atleast 30 minutes.
// Not a reaction, not a message, not any event!
bot.dispatchedGuildIds.add(guild.id);
return true;
},
interval: 3660000,
bot,
});
bot.channels.startSweeper({
filter: function channelSweeper(
channel,
key,
bot: BotWithCache<B>,
) {
// If this is in a guild and the guild was dispatched, then we can dispatch the channel
if (channel.guildId && bot.dispatchedGuildIds.has(channel.guildId)) {
bot.dispatchedChannelIds.add(channel.id);
return true;
}
// THE KEY DM CHANNELS ARE STORED BY IS THE USER ID. If the user is not cached, we dont need to cache their dm channel.
if (!channel.guildId && !bot.members.has(key)) return true;
return false;
},
interval: 3660000,
bot,
});
bot.members.startSweeper({
filter: function memberSweeper(member, _, bot: BotWithCache<B>) {
// Don't sweep the bot else strange things will happen
if (member.id === bot.id) return false;
// Only sweep members who were not active the last 30 minutes
return Date.now() - member.cachedAt > 1800000;
},
interval: 300000,
bot,
});
bot.messages.startSweeper({
filter: function messageSweeper(message) {
// DM messages aren't needed
if (!message.guildId) return true;
// Only delete messages older than 10 minutes
return Date.now() - message.timestamp > 600000;
},
interval: 300000,
bot,
});
bot.presences.startSweeper({ filter: () => true, interval: 300000, bot });
// DISPATCH REQUIREMENTS
const handleDiscordPayloadOld = bot.gateway.handleDiscordPayload;
bot.gateway.handleDiscordPayload = async function (_, data, shardId) {
// RUN DISPATCH CHECK
await dispatchRequirements(bot, data);
// RUN OLD HANDLER
handleDiscordPayloadOld(_, data, shardId);
};
}

View File

@@ -0,0 +1,32 @@
# fileloader-plugin
> Please check `./deps.ts` for the compatible `discordeno` version!
This plugin leverages the ability to write files, and then import them.
## Code Example
```typescript
import { createBot, enableFileLoaderPlugin, startBot } from './deps.ts' // Import discordeno and this plugin.
console.log('Starting Up the Bot, this might take awhile...');
const bot = enableFileLoaderPlugin(createBot({
token: '', // Your bot's token
botId: 0n, // Your bot's "Application Id",
intents: [],
events: {
ready() {
console.log('Bot Ready');
}
}
}));
bot.fastFileLoader([
// './src/commands', etc. This works just like `import [something] from [somewhere]`
]);
startBot(bot);
```
Make sure to ignore `fileloader.ts` in git as it is (re)generated whever you (re)start the bot.

View File

@@ -0,0 +1 @@
export * from "../../mod.ts";

99
plugins/fileloader/mod.ts Normal file
View File

@@ -0,0 +1,99 @@
import { Bot } from './deps.ts';
// iMpOrTaNt to make sure files can be reloaded properly!
export let uniqueFilePathCounter = 0;
export let paths: string[] = [];
/** Recursively generates an array of unique paths to import using `fileLoader()`
* (**Is** windows compatible)
*/
export async function importDirectory(path: string) {
path = path.replaceAll("\\", "/");
const files = Deno.readDirSync(Deno.realPathSync(path));
for (const file of files) {
if (!file.name) continue;
const currentPath = `${path}/${file.name}`;
if (file.isFile) {
if (!currentPath.endsWith(".ts")) continue;
paths.push(
`import "${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf("/"))}/${currentPath.substring(
currentPath.indexOf("src/")
)}#${uniqueFilePathCounter}";`
);
continue;
}
// Recursive function!
await importDirectory(currentPath);
}
uniqueFilePathCounter++;
}
/** Writes, then imports all everything in fileloader.ts */
export async function fileLoader() {
await Deno.writeTextFile("fileloader.ts", paths.join("\n").replaceAll("\\", "/"));
await import(
`${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf("/"))}/fileloader.ts#${uniqueFilePathCounter}`
);
paths = [];
}
/** This function will import the specified directories */
export async function fastFileLoader(
/** An array of directories to import recursively. */
paths: string[],
/** A function that will run before recursively setting a part of `paths`.
* `path` contains the path that will be imported, useful for logging
*/
between?: (path: string, uniqueFilePathCounter: number, paths: string[]) => void,
/** A function that runs before **actually** importing all the files. */
before?: (uniqueFilePathCounter: number, paths: string[]) => void
) {
await Promise.all(
[...paths].map((path) => {
if (between) between(path, uniqueFilePathCounter, paths);
importDirectory(path)
})
);
if (before) before(uniqueFilePathCounter, paths);
await fileLoader();
}
/** Extend the Bot with the Plugin's added functions */
export interface BotWithFileLoader extends Bot {
/** Recursively generates an array of unique paths to import using `fileLoader()`
* (**Is** windows compatible)
*/
importDirectory: (path: string) => void,
/** Writes, then imports all everything in fileloader.ts */
fileLoader: () => void,
/** This function will import the specified directories */
fastFileLoader: (
/** An array of directories to import recursively. */
paths: string[],
/** A function that will run before recursively setting a part of `paths`.
* `path` contains the path that will be imported, useful for logging
*/
between?: (path: string, uniqueFilePathCounter: number, paths: string[]) => void,
/** A function that runs before **actually** importing all the files. */
before?: (uniqueFilePathCounter: number, paths: string[]) => void
) => void,
}
/** Pass in a (compatible) bot instance, and get sweet file loader goodness.
* Remember to capture the output of this function!
*/
export function enableFileLoaderPlugin(rawBot: Bot): BotWithFileLoader {
const bot = rawBot as BotWithFileLoader;
bot.importDirectory = importDirectory;
bot.fileLoader = fileLoader;
bot.fastFileLoader = fastFileLoader;
return bot;
}

View File

@@ -0,0 +1 @@
# helpers-plugin

1
plugins/helpers/deps.ts Normal file
View File

@@ -0,0 +1 @@
export * from "../../mod.ts";

123
plugins/helpers/mod.ts Normal file
View File

@@ -0,0 +1,123 @@
import {
ApplicationCommandOptionChoice,
Bot,
Collection,
CreateMessage,
DiscordenoChannel,
DiscordenoMember,
DiscordenoMessage,
FinalHelpers,
ListGuildMembers,
ModifyThread,
} from "./deps.ts";
import { cloneChannel } from "./src/channels.ts";
import { sendAutocompleteChoices } from "./src/sendAutoCompleteChoices.ts";
import { sendDirectMessage } from "./src/sendDirectMessage.ts";
import { suppressEmbeds } from "./src/suppressEmbeds.ts";
import {
archiveThread,
editThread,
lockThread,
unarchiveThread,
unlockThread,
} from "./src/threads.ts";
import { disconnectMember } from "./src/disconnectMember.ts";
import { getMembersPaginated } from "./src/getMembersPaginated.ts";
import { moveMember } from "./src/moveMember.ts";
export interface BotWithHelpersPlugin extends Bot {
helpers: FinalHelpers & {
sendDirectMessage: (
userId: bigint,
content: string | CreateMessage
) => Promise<DiscordenoMessage>;
suppressEmbeds: (
channelId: bigint,
messageId: bigint
) => Promise<DiscordenoMessage>;
archiveThread: (threadId: bigint) => Promise<DiscordenoChannel>;
unarchiveThread: (threadId: bigint) => Promise<DiscordenoChannel>;
lockThread: (threadId: bigint) => Promise<DiscordenoChannel>;
unlockThread: (threadId: bigint) => Promise<DiscordenoChannel>;
editThread: (
threadId: bigint,
options: ModifyThread,
reason?: string
) => Promise<DiscordenoChannel>;
cloneChannel: (
channel: DiscordenoChannel,
reason?: string
) => Promise<DiscordenoChannel>;
sendAutocompleteChoices: (
interactionId: bigint,
interactionToken: string,
choices: ApplicationCommandOptionChoice[]
) => Promise<void>;
disconnectMember: (
guildId: bigint,
memberId: bigint
) => Promise<DiscordenoMember>;
getMembersPaginated: (
guildId: bigint,
options: ListGuildMembers & { memberCount: number }
) => Promise<Collection<bigint, DiscordenoMember>>;
moveMember: (
guildId: bigint,
memberId: bigint,
channelId: bigint
) => Promise<DiscordenoMember>;
};
}
export function enableHelpersPlugin(rawBot: Bot): BotWithHelpersPlugin {
const bot = rawBot as BotWithHelpersPlugin;
bot.helpers.sendDirectMessage = (
userId: bigint,
content: string | CreateMessage
) => sendDirectMessage(bot, userId, content);
bot.helpers.suppressEmbeds = (channelId: bigint, messageId: bigint) =>
suppressEmbeds(bot, channelId, messageId);
bot.helpers.archiveThread = (threadId: bigint) =>
archiveThread(bot, threadId);
bot.helpers.unarchiveThread = (threadId: bigint) =>
unarchiveThread(bot, threadId);
bot.helpers.lockThread = (threadId: bigint) => lockThread(bot, threadId);
bot.helpers.unlockThread = (threadId: bigint) => unlockThread(bot, threadId);
bot.helpers.editThread = (
threadId: bigint,
options: ModifyThread,
reason?: string
) => editThread(bot, threadId, options, reason);
bot.helpers.cloneChannel = (channel: DiscordenoChannel, reason?: string) =>
cloneChannel(bot, channel, reason);
bot.helpers.sendAutocompleteChoices = (
interactionId: bigint,
interactionToken: string,
choices: ApplicationCommandOptionChoice[]
) => sendAutocompleteChoices(bot, interactionId, interactionToken, choices);
bot.helpers.disconnectMember = (guildId: bigint, memberId: bigint) =>
disconnectMember(bot, guildId, memberId);
bot.helpers.getMembersPaginated = (
guildId: bigint,
options: ListGuildMembers & { memberCount: number }
) => getMembersPaginated(bot, guildId, options);
bot.helpers.moveMember = (
guildId: bigint,
memberId: bigint,
channelId: bigint
) => moveMember(bot, guildId, memberId, channelId);
return bot as BotWithHelpersPlugin;
}
// EXPORT EVERYTHING HERE SO USERS CAN OPT TO USE FUNCTIONS DIRECTLY
export * from "./src/channels.ts";
export * from "./src/disconnectMember.ts";
export * from "./src/getMembersPaginated.ts";
export * from "./src/moveMember.ts";
export * from "./src/sendAutoCompleteChoices.ts";
export * from "./src/sendDirectMessage.ts";
export * from "./src/suppressEmbeds.ts";
export * from "./src/threads.ts";
export default enableHelpersPlugin;

View File

@@ -0,0 +1,46 @@
import {
Bot,
CreateGuildChannel,
DiscordenoChannel,
separateOverwrites,
} from "../deps.ts";
/** Create a copy of a channel */
export async function cloneChannel(
bot: Bot,
channel: DiscordenoChannel,
reason?: string,
) {
if (!channel.guildId) {
throw new Error(`Cannot clone a channel outside a guild`);
}
const createChannelOptions: CreateGuildChannel = {
type: channel.type,
bitrate: channel.bitrate,
userLimit: channel.userLimit,
rateLimitPerUser: channel.rateLimitPerUser,
position: channel.position,
parentId: channel.parentId,
nsfw: channel.nsfw,
name: channel.name!,
topic: channel.topic || undefined,
permissionOverwrites: channel.permissionOverwrites.map((overwrite) => {
const [type, id, allow, deny] = separateOverwrites(overwrite);
return {
id,
type,
allow: bot.utils.calculatePermissions(BigInt(allow)),
deny: bot.utils.calculatePermissions(BigInt(deny)),
};
}),
};
//Create the channel (also handles permissions)
return await bot.helpers.createChannel(
channel.guildId!,
createChannelOptions,
reason,
);
}

View File

@@ -0,0 +1,6 @@
import { Bot } from "../deps.ts";
/** Kicks a member from a voice channel */
export function disconnectMember(bot: Bot, guildId: bigint, memberId: bigint) {
return bot.helpers.editMember(guildId, memberId, { channelId: null });
}

View File

@@ -0,0 +1,73 @@
import {
Bot,
Collection,
DiscordenoMember,
GuildMemberWithUser,
ListGuildMembers,
} from "../deps.ts";
/**
* 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 getMembersPaginated(
bot: Bot,
guildId: bigint,
options: ListGuildMembers & { memberCount: number }
) {
const members = new Collection<bigint, DiscordenoMember>();
let membersLeft = options?.limit ?? options.memberCount;
let loops = 1;
while (
(options?.limit ?? options.memberCount) > members.size &&
membersLeft > 0
) {
bot.events.debug("Running while loop in getMembers function.");
if (options?.limit && options.limit > 1000) {
console.log(
`Paginating get members from REST. #${loops} / ${Math.ceil(
(options?.limit ?? 1) / 1000
)}`
);
}
const result = await bot.rest.runMethod<GuildMemberWithUser[]>(
bot.rest,
"get",
`${bot.constants.endpoints.GUILD_MEMBERS(guildId)}?limit=${
membersLeft > 1000 ? 1000 : membersLeft
}${options?.after ? `&after=${options.after}` : ""}`
);
const discordenoMembers = result.map((member) =>
bot.transformers.member(
bot,
member,
guildId,
bot.transformers.snowflake(member.user.id)
)
);
if (!discordenoMembers.length) break;
discordenoMembers.forEach((member) => {
bot.events.debug(`Running forEach loop in get_members file.`);
members.set(member.id, member);
});
options = {
limit: options?.limit,
after: discordenoMembers[discordenoMembers.length - 1].id.toString(),
memberCount: options.memberCount,
};
membersLeft -= 1000;
loops++;
}
return members;
}

View File

@@ -0,0 +1,13 @@
import { Bot } from "../deps.ts";
/**
* Move a member from a voice channel to another.
*/
export function moveMember(
bot: Bot,
guildId: bigint,
memberId: bigint,
channelId: bigint
) {
return bot.helpers.editMember(guildId, memberId, { channelId });
}

View File

@@ -0,0 +1,19 @@
import {
ApplicationCommandOptionChoice,
Bot,
InteractionResponseTypes,
} from "../deps.ts";
export async function sendAutocompleteChoices(
bot: Bot,
interactionId: bigint,
interactionToken: string,
choices: ApplicationCommandOptionChoice[]
): Promise<void> {
await bot.helpers.sendInteractionResponse(interactionId, interactionToken, {
type: InteractionResponseTypes.ApplicationCommandAutocompleteResult,
data: {
choices: choices,
},
});
}

View File

@@ -0,0 +1,27 @@
import { Bot, Collection, CreateMessage } from "../deps.ts";
/** Maps the <userId, channelId> for dm channels */
export const dmChannelIds = new Collection<bigint, bigint>();
/** Sends a direct message to a user. This can take two API calls. The first call is to create a dm channel. Then sending the message to that channel. Channel ids are cached as needed to prevent duplicate requests. */
export async function sendDirectMessage(
bot: Bot,
userId: bigint,
content: string | CreateMessage,
) {
if (typeof content === "string") content = { content };
// GET CHANNEL ID FROM CACHE OR CREATE THE CHANNEL FOR THIS USER
const cachedChannelId = dmChannelIds.get(userId);
// IF ID IS CACHED SEND MESSAGE DIRECTLY
if (cachedChannelId) return bot.helpers.sendMessage(cachedChannelId, content);
// CREATE A NEW DM CHANNEL AND PULCK ITS ID
const channel = (await bot.helpers.getDmChannel(userId));
// CACHE IT FOR FUTURE REQUESTS
dmChannelIds.set(userId, channel.id);
// CACHE CHANNEL IF NEEDED
return bot.helpers.sendMessage(channel.id, content);
}

View File

@@ -0,0 +1,17 @@
import { Bot, Message } from "../deps.ts";
/** Suppress all the embeds in this message */
export async function suppressEmbeds(
bot: Bot,
channelId: bigint,
messageId: bigint,
) {
const result = await bot.rest.runMethod<Message>(
bot.rest,
"patch",
bot.constants.endpoints.CHANNEL_MESSAGE(channelId, messageId),
{ flags: 4 },
);
return bot.transformers.message(bot, result);
}

View File

@@ -0,0 +1,38 @@
import { Bot, Channel, ModifyThread } from "../deps.ts";
/** Sets a thread channel to be archived. */
export async function archiveThread(bot: Bot, threadId: bigint) {
return await editThread(bot, threadId, { archived: true });
}
/** Sets a thread channel to be unarchived. */
export async function unarchiveThread(bot: Bot, threadId: bigint) {
return await editThread(bot, threadId, { archived: false });
}
/** Sets a thread channel to be locked. */
export async function lockThread(bot: Bot, threadId: bigint) {
return await editThread(bot, threadId, { locked: true });
}
/** Sets a thread channel to be unlocked. */
export async function unlockThread(bot: Bot, threadId: bigint) {
return await editThread(bot, threadId, { locked: false });
}
/** Update a thread's settings. Requires the `MANAGE_CHANNELS` permission for the guild. */
export async function editThread(bot: Bot, threadId: bigint, options: ModifyThread, reason?: string) {
const result = await bot.rest.runMethod<Channel>(bot.rest, "patch", bot.constants.endpoints.CHANNEL_BASE(threadId), {
name: options.name,
archived: options.archived,
auto_archive_duration: options.autoArchiveDuration,
locked: options.locked,
rate_limit_per_user: options.rateLimitPerUser,
reason,
});
return bot.transformers.channel(bot, {
channel: result,
guildId: result.guild_id ? bot.transformers.snowflake(result.guild_id) : undefined,
});
}

View File

@@ -0,0 +1,25 @@
# permissions-plugin
This is an official plugin maintained by Discordeno. This plugin provides automatic permission checking and useful permission checking utility functions. Highly recommended to install this plugin for all users as you can use the utility functions. Enabling the permission plugin should not be done for big bot developers as it requires the cache plugin which will not work in a performance optimized fashion. This is designed mainly for the small beginner devs.
## Requirements
- [Cache Plugin](https://github.com/discordeno/cache-plugin)
## Usage
```ts
// MOVE TO DEPS.TS AND USE SPECIFIC VERSION
import enableCachePlugin from "https://deno.land/x/discordeno_cache_plugin/mod.ts";
import enablePermissionPlugin from "https://deno.land/x/discordeno_permission_plugin/mod.ts";
// Create the bot object, THIS WILL NEED YOUR OPTIONS.
const bot = createBot({});
// REQUIRED: Enables the cache plugin on this bot
enableCachePlugin(bot);
// Enables the permission plugin on this bot
enablePermissionPlugin(bot);
// Start your bot
await startBot(bot);
```

View File

@@ -0,0 +1,2 @@
export * from "../../mod.ts";
export type { BotWithCache } from "../cache/mod.ts";

View File

@@ -0,0 +1,48 @@
import { BotWithCache } from "./deps.ts";
import setupChannelPermChecks from "./src/channels/mod.ts";
import setupDiscoveryPermChecks from "./src/discovery.ts";
import setupEditMember from "./src/editMember.ts";
import setupEmojiPermChecks from "./src/emojis.ts";
import setupGuildPermChecks from "./src/guilds/mod.ts";
import setupIntegrationPermChecks from "./src/integrations.ts";
import setupInteractionPermChecks from "./src/interactions/mod.ts";
import setupInvitesPermChecks from "./src/invites.ts";
import setupMemberPermChecks from "./src/members/mod.ts";
import setupMessagePermChecks from "./src/messages/mod.ts";
import setupMiscPermChecks from "./src/misc/mod.ts";
import setupRolePermChecks from "./src/roles/mod.ts";
import setupWebhooksPermChecks from "./src/webhooks/mod.ts";
// PLUGINS MUST TAKE A BOT ARGUMENT WHICH WILL BE MODIFIED
export function enablePermissionsPlugin(bot: BotWithCache) {
// PERM CHECKS REQUIRE CACHE DUH!
if (!bot.enabledPlugins?.has("CACHE")) {
throw new Error("The PERMISSIONS plugin requires the CACHE plugin first.");
}
// MARK THIS PLUGIN BEING USED
bot.enabledPlugins.add("PERMISSIONS");
// BEGIN OVERRIDING HELPER FUNCTIONS
setupChannelPermChecks(bot);
setupDiscoveryPermChecks(bot);
setupEmojiPermChecks(bot);
setupEditMember(bot);
setupGuildPermChecks(bot);
setupIntegrationPermChecks(bot);
setupInteractionPermChecks(bot);
setupInvitesPermChecks(bot);
setupMemberPermChecks(bot);
setupMessagePermChecks(bot);
setupMiscPermChecks(bot);
setupRolePermChecks(bot);
setupWebhooksPermChecks(bot);
// PLUGINS MUST RETURN THE BOT
return bot;
}
// EXPORT ALL UTIL FUNCTIONS
export * from "./src/permissions.ts";
// DEFAULT MAKES IT SLIGHTLY EASIER TO USE
export default enablePermissionsPlugin;

View File

@@ -0,0 +1,37 @@
import { BotWithCache, ChannelTypes } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function deleteChannel(bot: BotWithCache) {
const deleteChannelOld = bot.helpers.deleteChannel;
bot.helpers.deleteChannel = function (channelId, reason) {
const channel = bot.channels.get(channelId);
if (channel?.guildId) {
const guild = bot.guilds.get(channel.guildId);
if (!guild) throw new Error("GUILD_NOT_FOUND");
if (guild.rulesChannelId === channelId) {
throw new Error("RULES_CHANNEL_CANNOT_BE_DELETED");
}
if (guild.publicUpdatesChannelId === channelId) {
throw new Error("UPDATES_CHANNEL_CANNOT_BE_DELETED");
}
const isThread = [
ChannelTypes.GuildNewsThread,
ChannelTypes.GuildPublicThread,
ChannelTypes.GuildPrivateThread,
].includes(channel.type);
requireBotGuildPermissions(
bot,
guild,
isThread ? ["MANAGE_THREADS"] : ["MANAGE_CHANNELS"],
);
}
return deleteChannelOld(channelId, reason);
};
}

View File

@@ -0,0 +1,16 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export default function deleteChannelOverwrite(bot: BotWithCache) {
const deleteChannelOverwriteOld = bot.helpers.deleteChannelOverwrite;
bot.helpers.deleteChannelOverwrite = function (channelId, overwriteId) {
const channel = bot.channels.get(channelId);
if (channel?.guildId) {
requireBotChannelPermissions(bot, channelId, ["MANAGE_ROLES"]);
}
return deleteChannelOverwriteOld(channelId, overwriteId);
};
}

View File

@@ -0,0 +1,123 @@
import { PermissionStrings } from "../../deps.ts";
import { BotWithCache, ChannelTypes, GuildFeatures } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export default function editChannel(bot: BotWithCache) {
const editChannelOld = bot.helpers.editChannel;
bot.helpers.editChannel = function (channelId, options, reason) {
const channel = bot.channels.get(channelId);
if (channel?.guildId) {
const guild = bot.guilds.get(channel.guildId);
if (options.rateLimitPerUser && options.rateLimitPerUser > 21600) {
throw new Error(
"Amount of seconds a user has to wait before sending another message must be between 0-21600",
);
}
if (options.name) {
if (!bot.utils.validateLength(options.name, { min: 1, max: 100 })) {
throw new Error(
"The channel name must be between 1-100 characters.",
);
}
}
const isThread = [
ChannelTypes.GuildNewsThread,
ChannelTypes.GuildPublicThread,
ChannelTypes.GuildPrivateThread,
].includes(channel.type);
const requiredPerms: PermissionStrings[] = [];
if (isThread) {
if (
options.invitable !== undefined &&
channel.type !== ChannelTypes.GuildPrivateThread
) {
throw new Error(
"Invitable option is only allowed on private threads.",
);
}
// UNARCHIVING AN UNLOCKED CHANNEL SIMPLY REQUIRES SEND
if (!channel.locked && options.archived === false) {
requiredPerms.push("SEND_MESSAGES");
// MORE THAN ARCHIVE WAS MODIFIED
if (Object.keys(options).length > 1) {
requiredPerms.push("MANAGE_THREADS");
}
} else {
requiredPerms.push("MANAGE_THREADS");
}
} else {
requiredPerms.push("MANAGE_CHANNELS");
if (options.permissionOverwrites) {
requiredPerms.push("MANAGE_ROLES");
}
if (options.type) {
if (
[ChannelTypes.GuildNews, ChannelTypes.GuildText].includes(
options.type,
)
) {
throw new Error("Only news and text types can be modified.");
}
if (guild && !guild.features.includes(GuildFeatures.News)) {
throw new Error(
"The NEWS feature is missing in this guild to be able to modify the channel type.",
);
}
}
if (options.topic) {
if (!bot.utils.validateLength(options.topic, { min: 1, max: 1024 })) {
throw new Error("The topic must be a number between 1 and 1024");
}
}
if (options.userLimit && options.userLimit > 99) {
throw new Error("The user limit must be less than 99.");
}
if (options.parentId) {
const category = bot.channels.get(options.parentId);
if (category && category.type !== ChannelTypes.GuildCategory) {
throw new Error(
"The parent id must be for a category channel type.",
);
}
}
}
requireBotChannelPermissions(
bot,
channel,
requiredPerms,
);
if (options.autoArchiveDuration) {
if (guild) {
if (
!guild.features.includes(
options.autoArchiveDuration === 4320
? GuildFeatures.ThreeDayThreadArchive
: GuildFeatures.SevenDayThreadArchive,
)
) {
throw new Error(
"The 3 day and 7 day archive durations require the server to be boosted",
);
}
}
}
}
return editChannelOld(channelId, options, reason);
};
}

View File

@@ -0,0 +1,15 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export default function editChannelOverwrite(bot: BotWithCache) {
const editChannelOverwriteOld = bot.helpers.editChannelOverwrite;
bot.helpers.editChannelOverwrite = function (channelId, overwriteId, options) {
const channel = bot.channels.get(channelId);
if (channel?.guildId) {
requireBotChannelPermissions(bot, channelId, ["MANAGE_ROLES"]);
}
return editChannelOverwriteOld(channelId, overwriteId, options);
};
}

View File

@@ -0,0 +1,15 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export default function followChannel(bot: BotWithCache) {
const followChannelOld = bot.helpers.followChannel;
bot.helpers.followChannel = function (sourceChannelId, targetChannelId) {
const channel = bot.channels.get(targetChannelId);
if (channel?.guildId) {
requireBotChannelPermissions(bot, channel, ["MANAGE_WEBHOOKS"]);
}
return followChannelOld(sourceChannelId, targetChannelId);
};
}

View File

@@ -0,0 +1,15 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export default function getChannelWebhooks(bot: BotWithCache) {
const getChannelWebhooksOld = bot.helpers.getChannelWebhooks;
bot.helpers.getChannelWebhooks = function (channelId) {
const channel = bot.channels.get(channelId);
if (channel?.guildId) {
requireBotChannelPermissions(bot, channelId, ["MANAGE_WEBHOOKS"]);
}
return getChannelWebhooksOld(channelId);
};
}

View File

@@ -0,0 +1,22 @@
import { BotWithCache } from "../../deps.ts";
import setupThreadPermChecks from "./threads/mod.ts";
import setupStagePermChecks from "./stage.ts";
import deleteChannel from "./deleteChannel.ts";
import deleteChannelOverwrite from "./deleteChannelOverwrite.ts";
import editChannel from "./editChannel.ts";
import editChannelOverwrite from "./editChannelOverwrite.ts";
import followChannel from "./followChannel.ts";
import getChannelWebhooks from "./getChannelWebhooks.ts";
import swapChannels from "./swapChannels.ts";
export default function setupChannelPermChecks(bot: BotWithCache) {
setupThreadPermChecks(bot);
setupStagePermChecks(bot);
deleteChannel(bot);
deleteChannelOverwrite(bot);
editChannel(bot);
editChannelOverwrite(bot);
followChannel(bot);
getChannelWebhooks(bot);
swapChannels(bot);
}

View File

@@ -0,0 +1,56 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export function createStageInstance(bot: BotWithCache) {
const createStageInstanceOld = bot.helpers.createStageInstance;
bot.helpers.createStageInstance = function (channelId, topic, privacyLevel) {
if (!bot.utils.validateLength(topic, { max: 120, min: 1 })) {
throw new Error(
"The topic length for creating a stage instance must be between 1-120.",
);
}
requireBotChannelPermissions(bot, channelId, [
"MANAGE_CHANNELS",
"MUTE_MEMBERS",
"MOVE_MEMBERS",
]);
return createStageInstanceOld(channelId, topic, privacyLevel);
};
}
export function deleteStageInstance(bot: BotWithCache) {
const deleteStageInstanceOld = bot.helpers.deleteStageInstance;
bot.helpers.deleteStageInstance = function (channelId) {
requireBotChannelPermissions(bot, channelId, [
"MANAGE_CHANNELS",
"MUTE_MEMBERS",
"MOVE_MEMBERS",
]);
return deleteStageInstanceOld(channelId);
};
}
export function updateStageInstance(bot: BotWithCache) {
const updateStageInstanceOld = bot.helpers.updateStageInstance;
bot.helpers.updateStageInstance = function (channelId, data) {
requireBotChannelPermissions(bot, channelId, [
"MANAGE_CHANNELS",
"MUTE_MEMBERS",
"MOVE_MEMBERS",
]);
return updateStageInstanceOld(channelId, data);
};
}
export default function setupStagePermChecks(bot: BotWithCache) {
createStageInstance(bot);
deleteStageInstance(bot);
updateStageInstance(bot);
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function swapChannels(bot: BotWithCache) {
const swapChannelsOld = bot.helpers.swapChannels;
bot.helpers.swapChannels = function (guildId, channelPositions) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_CHANNELS"]);
return swapChannelsOld(guildId, channelPositions);
};
}

View File

@@ -0,0 +1,26 @@
import { BotWithCache } from "../../../deps.ts";
import { requireBotChannelPermissions } from "../../permissions.ts";
export default function addToThread(bot: BotWithCache) {
const addToThreadOld = bot.helpers.addToThread;
bot.helpers.addToThread = async function (threadId, userId) {
if (userId === bot.id) {
throw new Error(
"To add the bot to a thread, you must use bot.helpers.joinThread()",
);
}
const channel = bot.channels.get(threadId);
if (channel) {
if (channel.archived) {
throw new Error("Cannot add user to thread if thread is archived.");
}
await requireBotChannelPermissions(bot, channel, ["SEND_MESSAGES"]);
}
return addToThreadOld(threadId, userId);
};
}

View File

@@ -0,0 +1,22 @@
import { BotWithCache } from "../../../deps.ts";
import { requireBotChannelPermissions } from "../../permissions.ts";
export default function getArchivedThreads(bot: BotWithCache) {
const getArchivedThreadsOld = bot.helpers.getArchivedThreads;
bot.helpers.getArchivedThreads = async function (channelId, options) {
const channel = await bot.channels.get(channelId);
if (channel) {
await requireBotChannelPermissions(
bot,
channel,
options?.type === "private"
? ["READ_MESSAGE_HISTORY", "MANAGE_THREADS"]
: ["READ_MESSAGE_HISTORY"],
);
}
return getArchivedThreadsOld(channelId, options);
};
}

View File

@@ -0,0 +1,16 @@
import { BotWithCache, GatewayIntents } from "../../../deps.ts";
export default function getThreadMembers(bot: BotWithCache) {
const getThreadMembersOld = bot.helpers.getThreadMembers;
bot.helpers.getThreadMembers = function (threadId) {
const hasIntent = bot.intents & GatewayIntents.GuildMembers;
if (!hasIntent) {
throw new Error(
"The get thread members endpoint requires GuildMembers intent.",
);
}
return getThreadMembersOld(threadId);
};
}

View File

@@ -0,0 +1,15 @@
import { BotWithCache } from "../../../deps.ts";
export default function joinThread(bot: BotWithCache) {
const joinThreadOld = bot.helpers.joinThread;
bot.helpers.joinThread = function (threadId) {
const channel = bot.channels.get(threadId);
if (channel && !channel.archived) {
throw new Error("You can not join an archived channel.");
}
return joinThreadOld(threadId);
};
}

View File

@@ -0,0 +1,15 @@
import { BotWithCache } from "../../../deps.ts";
export default function leaveThread(bot: BotWithCache) {
const leaveThreadOld = bot.helpers.leaveThread;
bot.helpers.leaveThread = function (threadId) {
const channel = bot.channels.get(threadId);
if (channel && !channel.archived) {
throw new Error("You can not leave an archived channel.");
}
return leaveThreadOld(threadId);
};
}

View File

@@ -0,0 +1,16 @@
import { BotWithCache } from "../../../deps.ts";
import addToThread from "./addToThread.ts";
import getArchivedThreads from "./getArchivedThreads.ts";
import getThreadMembers from "./getThreadMembers.ts";
import joinThread from "./joinThread.ts";
import leaveThread from "./leaveThread.ts";
import removeThreadMember from "./removeThreadMember.ts";
export default function setupThreadPermChecks(bot: BotWithCache) {
addToThread(bot);
getArchivedThreads(bot);
getThreadMembers(bot);
joinThread(bot);
leaveThread(bot);
removeThreadMember(bot);
}

View File

@@ -0,0 +1,33 @@
import { BotWithCache, ChannelTypes } from "../../../deps.ts";
import { requireBotChannelPermissions } from "../../permissions.ts";
export default function removeThreadMember(bot: BotWithCache) {
const removeThreadMemberOld = bot.helpers.removeThreadMember;
bot.helpers.removeThreadMember = async function (threadId, userId) {
if (userId === bot.id) {
throw new Error(
"To remove the bot from a thread, you must use bot.helpers.leaveThread()",
);
}
const channel = bot.channels.get(threadId);
if (channel) {
if (channel.archived) {
throw new Error(
"Cannot remove user from thread if thread is archived.",
);
}
if (
!(bot.id === channel.ownerId &&
channel.type === ChannelTypes.GuildPrivateThread)
) {
await requireBotChannelPermissions(bot, channel, ["MANAGE_MESSAGES"]);
}
}
return removeThreadMemberOld(threadId, userId);
};
}

View File

@@ -0,0 +1,178 @@
import {
Bot,
ButtonStyles,
MessageComponents,
MessageComponentTypes,
} from "../deps.ts";
export function validateComponents(bot: Bot, components: MessageComponents) {
if (!components?.length) return;
let actionRowCounter = 0;
for (const component of components) {
actionRowCounter++;
// Max of 5 ActionRows per message
if (actionRowCounter > 5) throw new Error("Too many action rows.");
// Max of 5 Buttons (or any component type) within an ActionRow
if (component.components?.length > 5) {
throw new Error("Too many components.");
} else if (
component.components?.length > 1 &&
component.components.some((subcomponent) =>
subcomponent.type === MessageComponentTypes.SelectMenu
)
) {
throw new Error("Select component must be alone.");
}
for (const subcomponent of component.components) {
if (
subcomponent.customId &&
!bot.utils.validateLength(subcomponent.customId, { max: 100 })
) {
throw new Error("The custom id in the component is too big.");
}
// 5 Link buttons can not have a customId
if (subcomponent.type === MessageComponentTypes.Button) {
if (subcomponent.style === ButtonStyles.Link && subcomponent.customId) {
throw new Error("Link buttons can not have custom ids.");
}
// Other buttons must have a customId
if (
!subcomponent.customId && subcomponent.style !== ButtonStyles.Link
) {
throw new Error(
"The button requires a custom id if it is not a link button.",
);
}
if (!bot.utils.validateLength(subcomponent.label, { max: 80 })) {
throw new Error("The label can not be longer than 80 characters.");
}
subcomponent.emoji = makeEmojiFromString(subcomponent.emoji);
}
if (subcomponent.type === MessageComponentTypes.SelectMenu) {
if (
subcomponent.placeholder &&
!bot.utils.validateLength(subcomponent.placeholder, { max: 100 })
) {
throw new Error(
"The component placeholder can not be longer than 100 characters.",
);
}
if (subcomponent.minValues) {
if (subcomponent.minValues < 1) {
throw new Error(
"The min values must be more than 1 in a select component.",
);
}
if (subcomponent.minValues > 25) {
throw new Error(
"The min values must be less than 25 in a select component.",
);
}
if (!subcomponent.maxValues) {
subcomponent.maxValues = subcomponent.minValues;
}
if (subcomponent.minValues > subcomponent.maxValues) {
throw new Error(
"The select component can not have a min values higher than a max values.",
);
}
}
if (subcomponent.maxValues) {
if (subcomponent.maxValues < 1) {
throw new Error(
"The max values must be more than 1 in a select component.",
);
}
if (subcomponent.maxValues > 25) {
throw new Error(
"The max values must be less than 25 in a select component.",
);
}
}
if (subcomponent.options.length < 1) {
throw new Error("You need atleast 1 option in the select component.");
}
if (subcomponent.options.length > 25) {
throw new Error(
"You can not have more than 25 options in the select component.",
);
}
let defaults = 0;
for (const option of subcomponent.options) {
if (option.default) {
defaults++;
if (defaults > (subcomponent.maxValues || 25)) {
throw new Error("You chose too many default options.");
}
}
if (!bot.utils.validateLength(option.label, { max: 25 })) {
throw new Error(
"The select component label can not exceed 25 characters.",
);
}
if (!bot.utils.validateLength(option.value, { max: 100 })) {
throw new Error(
"The select component value can not exceed 100 characters.",
);
}
if (
option.description &&
!bot.utils.validateLength(option.description, { max: 50 })
) {
throw new Error(
"The select option description can not exceed 50 characters.",
);
}
option.emoji = makeEmojiFromString(option.emoji);
}
}
}
}
}
function makeEmojiFromString(
emoji?:
| string
| {
id?: string | undefined;
name?: string | undefined;
animated?: boolean | undefined;
},
) {
if (typeof emoji !== "string") return emoji;
// A snowflake id was provided
if (/^[0-9]+$/.test(emoji)) {
emoji = {
id: emoji,
};
} else {
// A unicode emoji was provided
emoji = {
name: emoji,
};
}
return emoji;
}

View File

@@ -0,0 +1,43 @@
import { BotWithCache, ChannelTypes, PermissionStrings } from "../deps.ts";
import { requireBotChannelPermissions } from "./permissions.ts";
export default function connectToVoiceChannel(bot: BotWithCache) {
const connectToVoiceChannelOld = bot.helpers.connectToVoiceChannel;
bot.helpers.connectToVoiceChannel = async function (
guildId,
channelId,
options
) {
const channel = await bot.channels.get(channelId);
if (!channel) throw new Error("CHANNEL_NOT_FOUND");
if (
[ChannelTypes.GuildStageVoice, ChannelTypes.GuildVoice].includes(
channel.type
)
)
throw new Error("INVALID_CHANNEL_TYPE");
const guild = channel?.guildId && bot.guilds.get(channel.guildId);
if (!guild) throw new Error("GUILD_NOT_FOUND");
// Permissions needed for the bot to connect
// CONNECT is needed
const permsNeeded: PermissionStrings[] = ["CONNECT"];
// Check if there is space for the bot if channel has user limit
// Having MANAGE_CHANNELS permissions bypasses the limit
// --> Add MANAGE_CHANNELS perm to the check if it is needed
if (
channel.userLimit &&
guild.voiceStates.filter((vs) => vs.channelId === channelId).size >=
channel.userLimit
)
permsNeeded.push("MANAGE_CHANNELS");
await requireBotChannelPermissions(bot, channel, permsNeeded);
return connectToVoiceChannelOld(guildId, channelId, options);
};
}

View File

@@ -0,0 +1,49 @@
import { BotWithCache } from "../deps.ts";
import { requireBotGuildPermissions } from "./permissions.ts";
export function addDiscoverySubcategory(bot: BotWithCache) {
const addDiscoverySubcategoryOld = bot.helpers.addDiscoverySubcategory;
bot.helpers.addDiscoverySubcategory = function (guildId, categoryId) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]);
return addDiscoverySubcategoryOld(guildId, categoryId);
};
}
export function removeDiscoverySubcategory(bot: BotWithCache) {
const removeDiscoverySubcategoryOld = bot.helpers.removeDiscoverySubcategory;
bot.helpers.removeDiscoverySubcategory = function (guildId, categoryId) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]);
return removeDiscoverySubcategoryOld(guildId, categoryId);
};
}
export function getDiscovery(bot: BotWithCache) {
const getDiscoveryOld = bot.helpers.getDiscovery;
bot.helpers.getDiscovery = function (guildId) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]);
return getDiscoveryOld(guildId);
};
}
export function editDiscovery(bot: BotWithCache) {
const editDiscoveryOld = bot.helpers.editDiscovery;
bot.helpers.editDiscovery = function (guildId, data) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]);
return editDiscoveryOld(guildId, data);
};
}
export default function setupDiscoveryPermChecks(bot: BotWithCache) {
addDiscoverySubcategory(bot);
editDiscovery(bot);
getDiscovery(bot);
removeDiscoverySubcategory(bot);
}

View File

@@ -0,0 +1,67 @@
import { BotWithCache, PermissionStrings } from "../deps.ts";
import {
requireBotChannelPermissions,
requireBotGuildPermissions,
} from "./permissions.ts";
export default function editMember(bot: BotWithCache) {
const editMemberOld = bot.helpers.editMember;
bot.helpers.editMember = async function (guildId, memberId, options) {
const requiredPerms: Set<PermissionStrings> = new Set();
if (options.nick) {
if (options.nick.length > 32) {
throw new Error("NICKNAMES_MAX_LENGTH");
}
requiredPerms.add("MANAGE_NICKNAMES");
}
if (options.roles) requiredPerms.add("MANAGE_ROLES");
if (
options.mute !== undefined || options.deaf !== undefined ||
options.channelId !== undefined
) {
const memberVoiceState = (await bot.guilds.get(guildId))
?.voiceStates.get(memberId);
if (!memberVoiceState?.channelId) {
throw new Error("MEMBER_NOT_IN_VOICE_CHANNEL");
}
if (options.mute !== undefined) {
requiredPerms.add("MUTE_MEMBERS");
}
if (options.deaf !== undefined) {
requiredPerms.add("DEAFEN_MEMBERS");
}
if (options.channelId) {
const requiredVoicePerms: Set<PermissionStrings> = new Set([
"CONNECT",
"MOVE_MEMBERS",
]);
if (memberVoiceState) {
await requireBotChannelPermissions(
bot,
memberVoiceState?.channelId,
[
...requiredVoicePerms,
],
);
}
await requireBotChannelPermissions(bot, options.channelId, [
...requiredVoicePerms,
]);
}
}
await requireBotGuildPermissions(bot, guildId, [
...requiredPerms,
]);
return editMemberOld(guildId, memberId, options);
};
}

View File

@@ -0,0 +1,38 @@
import { BotWithCache } from "../deps.ts";
import { requireBotGuildPermissions } from "./permissions.ts";
export function createEmoji(bot: BotWithCache) {
const createEmojiOld = bot.helpers.createEmoji;
bot.helpers.createEmoji = function (guildId, id) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_EMOJIS"]);
return createEmojiOld(guildId, id);
};
}
export function deleteEmoji(bot: BotWithCache) {
const deleteEmojiOld = bot.helpers.deleteEmoji;
bot.helpers.deleteEmoji = function (guildId, id) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_EMOJIS"]);
return deleteEmojiOld(guildId, id);
};
}
export function editEmoji(bot: BotWithCache) {
const editEmojiOld = bot.helpers.editEmoji;
bot.helpers.editEmoji = function (guildId, id, options) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_EMOJIS"]);
return editEmojiOld(guildId, id, options);
};
}
export default function setupEmojiPermChecks(bot: BotWithCache) {
createEmoji(bot);
deleteEmoji(bot);
editEmoji(bot)
}

View File

@@ -0,0 +1,22 @@
import { BotWithCache } from "../../deps.ts";
export default function createGuild(bot: BotWithCache) {
const createGuildOld = bot.helpers.createGuild;
bot.helpers.createGuild = function (options) {
if (bot.guilds.size > 10) {
throw new Error(
"A bot can not create a guild if it is already in 10 guilds.",
);
}
if (
options.name &&
!bot.utils.validateLength(options.name, { min: 2, max: 100 })
) {
throw new Error("The guild name must be between 2 and 100 characters.");
}
return createGuildOld(options);
};
}

View File

@@ -0,0 +1,14 @@
import { BotWithCache } from "../../deps.ts";
export default function deleteGuild(bot: BotWithCache) {
const deleteGuildOld = bot.helpers.deleteGuild;
bot.helpers.deleteGuild = function (guildId) {
const guild = bot.guilds.get(guildId);
if (guild && guild.ownerId !== bot.id) {
throw new Error("A bot can only delete a guild it owns.");
}
return deleteGuildOld(guildId);
};
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function editGuild(bot: BotWithCache) {
const editGuildOld = bot.helpers.editGuild;
bot.helpers.editGuild = function (guildId, options, shardId) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"])
return editGuildOld(guildId, options, shardId);
};
}

View File

@@ -0,0 +1,144 @@
import { BotWithCache, ScheduledEventEntityType } from "../../deps.ts";
import {
requireBotChannelPermissions,
requireBotGuildPermissions,
} from "../permissions.ts";
export function createScheduledEvent(bot: BotWithCache) {
const createScheduledEventOld = bot.helpers.createScheduledEvent;
bot.helpers.createScheduledEvent = function (guildId, options) {
if (options.entityType === ScheduledEventEntityType.StageInstance) {
if (!options.channelId) {
throw new Error(
"A channel id is required for creating a stage scheduled event.",
);
}
requireBotChannelPermissions(bot, options.channelId, [
"MANAGE_CHANNELS",
"MUTE_MEMBERS",
"MOVE_MEMBERS",
]);
// MANAGE_EVENTS at the guild level or at least MANAGE_EVENTS for the channel_id associated with the event
try {
requireBotGuildPermissions(bot, guildId, [
"MANAGE_EVENTS",
]);
} catch {
requireBotChannelPermissions(bot, options.channelId, [
"MANAGE_EVENTS",
]);
}
return createScheduledEventOld(guildId, options);
}
if (options.entityType === ScheduledEventEntityType.Voice) {
if (!options.channelId) {
throw new Error(
"A channel id is required for creating a voice scheduled event.",
);
}
requireBotChannelPermissions(bot, options.channelId, [
"VIEW_CHANNEL",
"CONNECT",
]);
// MANAGE_EVENTS at the guild level or at least MANAGE_EVENTS for the channel_id associated with the event
try {
requireBotGuildPermissions(bot, guildId, [
"MANAGE_EVENTS",
]);
} catch {
requireBotChannelPermissions(bot, options.channelId, [
"MANAGE_EVENTS",
]);
}
return createScheduledEventOld(guildId, options);
}
// EXTERNAL EVENTS
requireBotGuildPermissions(bot, guildId, [
"MANAGE_EVENTS",
]);
return createScheduledEventOld(guildId, options);
};
}
export function editScheduledEvent(bot: BotWithCache) {
const editScheduledEventOld = bot.helpers.editScheduledEvent;
bot.helpers.editScheduledEvent = function (guildId, eventId, options) {
if (options.entityType === ScheduledEventEntityType.StageInstance) {
if (!options.channelId) {
throw new Error(
"A channel id is required for creating a stage scheduled event.",
);
}
requireBotChannelPermissions(bot, options.channelId, [
"MANAGE_CHANNELS",
"MUTE_MEMBERS",
"MOVE_MEMBERS",
]);
// MANAGE_EVENTS at the guild level or at least MANAGE_EVENTS for the channel_id associated with the event
try {
requireBotGuildPermissions(bot, guildId, [
"MANAGE_EVENTS",
]);
} catch {
requireBotChannelPermissions(bot, options.channelId, [
"MANAGE_EVENTS",
]);
}
return editScheduledEventOld(guildId, eventId, options);
}
if (options.entityType === ScheduledEventEntityType.Voice) {
if (!options.channelId) {
throw new Error(
"A channel id is required for creating a voice scheduled event.",
);
}
requireBotChannelPermissions(bot, options.channelId, [
"VIEW_CHANNEL",
"CONNECT",
]);
// MANAGE_EVENTS at the guild level or at least MANAGE_EVENTS for the channel_id associated with the event
try {
requireBotGuildPermissions(bot, guildId, [
"MANAGE_EVENTS",
]);
} catch {
requireBotChannelPermissions(bot, options.channelId, [
"MANAGE_EVENTS",
]);
}
return editScheduledEventOld(guildId, eventId, options);
}
// EXTERNAL EVENTS
requireBotGuildPermissions(bot, guildId, [
"MANAGE_EVENTS",
]);
return editScheduledEventOld(guildId, eventId, options);
};
}
export default function setupEventsPermChecks(bot: BotWithCache) {
createScheduledEvent(bot);
editScheduledEvent(bot);
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function getAuditLogs(bot: BotWithCache) {
const getAuditLogsOld = bot.helpers.getAuditLogs;
bot.helpers.getAuditLogs = function (guildId, options) {
requireBotGuildPermissions(bot, guildId, ["VIEW_AUDIT_LOG"]);
return getAuditLogsOld(guildId, options);
};
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function getBan(bot: BotWithCache) {
const getBanOld = bot.helpers.getBan;
bot.helpers.getBan = function (guildId, memberId) {
requireBotGuildPermissions(bot, guildId, ["BAN_MEMBERS"]);
return getBanOld(guildId, memberId);
};
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function getBans(bot: BotWithCache) {
const getBansOld = bot.helpers.getBans;
bot.helpers.getBans = function (guildId) {
requireBotGuildPermissions(bot, guildId, ["BAN_MEMBERS"]);
return getBansOld(guildId);
};
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function getPruneCount(bot: BotWithCache) {
const getPruneCountOld = bot.helpers.getPruneCount;
bot.helpers.getPruneCount = function (guildId, options) {
requireBotGuildPermissions(bot, guildId, ["KICK_MEMBERS"]);
return getPruneCountOld(guildId, options);
};
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function getVanityUrl(bot: BotWithCache) {
const getVanityUrlOld = bot.helpers.getVanityUrl;
bot.helpers.getVanityUrl = function (guildId) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]);
return getVanityUrlOld(guildId);
};
}

View File

@@ -0,0 +1,26 @@
import { BotWithCache } from "../../deps.ts";
import setupEventsPermChecks from "./events.ts";
import setupWelcomeScreenPermChecks from "./welcomeScreen.ts";
import setupWidgetPermChecks from "./widget.ts";
import createGuild from "./createGuild.ts";
import deleteGuild from "./deleteGuild.ts";
import editGuild from "./editGuild.ts";
import getAuditLogs from "./getAuditLogs.ts";
import getBan from "./getBan.ts";
import getBans from "./getBans.ts";
import getPruneCount from "./getPruneCount.ts";
import getVanityUrl from "./getVanityUrl.ts";
export default function setupGuildPermChecks(bot: BotWithCache) {
setupEventsPermChecks(bot);
createGuild(bot);
deleteGuild(bot);
editGuild(bot);
setupWelcomeScreenPermChecks(bot);
setupWidgetPermChecks(bot);
getAuditLogs(bot);
getBan(bot);
getBans(bot);
getPruneCount(bot);
getVanityUrl(bot);
}

View File

@@ -0,0 +1,16 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export function editWelcomeScreen(bot: BotWithCache) {
const editWelcomeScreenOld = bot.helpers.editWelcomeScreen;
bot.helpers.editWelcomeScreen = function (guildId, options) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]);
return editWelcomeScreenOld(guildId, options);
};
}
export default function setupWelcomeScreenPermChecks(bot: BotWithCache) {
editWelcomeScreen(bot);
}

View File

@@ -0,0 +1,16 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export function editWidget(bot: BotWithCache) {
const editWidgetOld = bot.helpers.editWidget;
bot.helpers.editWidget = function (guildId, enabled, channelId) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]);
return editWidgetOld(guildId, enabled, channelId);
};
}
export default function setupWidgetPermChecks(bot: BotWithCache) {
editWidget(bot);
}

View File

@@ -0,0 +1,27 @@
import { BotWithCache } from "../deps.ts";
import { requireBotGuildPermissions } from "./permissions.ts";
export function deleteIntegration(bot: BotWithCache) {
const deleteIntegrationOld = bot.helpers.deleteIntegration;
bot.helpers.deleteIntegration = function (guildId, id) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]);
return deleteIntegrationOld(guildId, id);
};
}
export function getIntegrations(bot: BotWithCache) {
const getIntegrationsOld = bot.helpers.getIntegrations;
bot.helpers.getIntegrations = function (guildId) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]);
return getIntegrationsOld(guildId);
};
}
export default function setupIntegrationPermChecks(bot: BotWithCache) {
deleteIntegration(bot);
getIntegrations(bot);
}

View File

@@ -0,0 +1,201 @@
import {
AllowedMentionsTypes,
ApplicationCommandOption,
ApplicationCommandOptionTypes,
ApplicationCommandTypes,
BotWithCache,
CONTEXT_MENU_COMMANDS_NAME_REGEX,
SLASH_COMMANDS_NAME_REGEX,
} from "../../deps.ts";
export function validateApplicationCommandOptions(
bot: BotWithCache,
options: ApplicationCommandOption[],
) {
const requiredOptions: ApplicationCommandOption[] = [];
const optionalOptions: ApplicationCommandOption[] = [];
for (const option of options) {
option.name = option.name.toLowerCase();
if (option.choices?.length) {
if (option.choices.length > 25) {
throw new Error("Too many application command options provided.");
}
if (
option.type !== ApplicationCommandOptionTypes.String &&
option.type !== ApplicationCommandOptionTypes.Integer
) {
throw new Error("Only string or integer options can have choices.");
}
}
if (!bot.utils.validateLength(option.name, { min: 1, max: 32 })) {
throw new Error("Invalid application command option name.");
}
if (!bot.utils.validateLength(option.description, { min: 1, max: 100 })) {
throw new Error("Invalid application command description.");
}
option.choices?.every((choice) => {
if (!bot.utils.validateLength(choice.name, { min: 1, max: 100 })) {
throw new Error(
"Invalid application command option choice name. Must be between 1-100 characters long.",
);
}
if (
option.type === ApplicationCommandOptionTypes.String &&
(typeof choice.value !== "string" || choice.value.length < 1 ||
choice.value.length > 100)
) {
throw new Error("Invalid slash options choice value type.");
}
if (
option.type === ApplicationCommandOptionTypes.Integer &&
typeof choice.value !== "number"
) {
throw new Error("A number must be set for Integer types.");
}
});
if (option.required) {
requiredOptions.push(option);
continue;
}
optionalOptions.push(option);
}
return [...requiredOptions, ...optionalOptions];
}
export function createApplicationCommand(bot: BotWithCache) {
const createApplicationCommandOld = bot.helpers.createApplicationCommand;
bot.helpers.createApplicationCommand = function (options, guildId) {
const isChatInput = !options.type ||
options.type === ApplicationCommandTypes.ChatInput;
if (!options.name) {
throw new Error("A name is required to create a options.");
}
if (isChatInput) {
if (!SLASH_COMMANDS_NAME_REGEX.test(options.name)) {
throw new Error(
"The name of the slash command did not match the required regex.",
);
}
// Only slash need to be lowercase
options.name = options.name.toLowerCase();
} else {
if (!CONTEXT_MENU_COMMANDS_NAME_REGEX.test(options.name)) {
throw new Error(
"The name of the context menu did not match the required regex.",
);
}
}
// Slash commands require description
if (
!options.description &&
(isChatInput)
) {
throw new Error(
"Slash commands require some form of a description be provided.",
);
}
if (
options.description &&
((options.type === ApplicationCommandTypes.User) ||
(options.type === ApplicationCommandTypes.Message))
) {
throw new Error("Context menu commands do not allow a description.");
}
if (
options.description &&
!bot.utils.validateLength(options.description, { min: 1, max: 100 })
) {
throw new Error(
"Application command descriptions must be between 1 and 100 characters.",
);
}
if (options.options?.length) {
if (options.options.length > 25) {
throw new Error("Only 25 options are allowed to be provided.");
}
options.options = validateApplicationCommandOptions(bot, options.options);
}
return createApplicationCommandOld(options, guildId);
};
}
export function editInteractionResponse(bot: BotWithCache) {
const editInteractionResponseOld = bot.helpers.editInteractionResponse;
bot.helpers.editInteractionResponse = function (token, options) {
if (options.content && options.content.length > 2000) {
throw Error(bot.constants.Errors.MESSAGE_MAX_LENGTH);
}
if (options.embeds && options.embeds.length > 10) {
options.embeds.splice(10);
}
if (options.allowedMentions) {
if (options.allowedMentions.users?.length) {
if (
options.allowedMentions.parse?.includes(
AllowedMentionsTypes.UserMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((
p,
) => p !== "users");
}
if (options.allowedMentions.users.length > 100) {
options.allowedMentions.users = options.allowedMentions.users.slice(
0,
100,
);
}
}
if (options.allowedMentions.roles?.length) {
if (
options.allowedMentions.parse?.includes(
AllowedMentionsTypes.RoleMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((
p,
) => p !== "roles");
}
if (options.allowedMentions.roles.length > 100) {
options.allowedMentions.roles = options.allowedMentions.roles.slice(
0,
100,
);
}
}
}
return editInteractionResponseOld(token, options);
};
}
export default function setupInteractionCommandPermChecks(bot: BotWithCache) {
createApplicationCommand(bot);
editInteractionResponse(bot);
}

View File

@@ -0,0 +1,61 @@
import { AllowedMentionsTypes, BotWithCache } from "../../deps.ts";
export default function editFollowupMessage(bot: BotWithCache) {
const editFollowupMessageOld = bot.helpers.editFollowupMessage;
bot.helpers.editFollowupMessage = function (
token,
messageId,
options,
) {
if (options.content && options.content.length > 2000) {
throw Error("MESSAGE_MAX_LENGTH");
}
if (options.embeds && options.embeds.length > 10) {
options.embeds.splice(10);
}
if (options.allowedMentions) {
if (options.allowedMentions.users?.length) {
if (
options.allowedMentions.parse?.includes(
AllowedMentionsTypes.UserMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((
p,
) => p !== "users");
}
if (options.allowedMentions.users.length > 100) {
options.allowedMentions.users = options.allowedMentions.users.slice(
0,
100,
);
}
}
if (options.allowedMentions.roles?.length) {
if (
options.allowedMentions.parse?.includes(
AllowedMentionsTypes.RoleMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((
p,
) => p !== "roles");
}
if (options.allowedMentions.roles.length > 100) {
options.allowedMentions.roles = options.allowedMentions.roles.slice(
0,
100,
);
}
}
}
return editFollowupMessageOld(token, messageId, options);
};
}

View File

@@ -0,0 +1,31 @@
import { BotWithCache } from "../../deps.ts";
import setupInteractionCommandPermChecks from "./commands.ts";
import editFollowupMessage from "./editFollowupMessage.ts";
export function sendInteractionResponse(bot: BotWithCache) {
const sendInteractionResponseOld = bot.helpers.sendInteractionResponse;
bot.helpers.sendInteractionResponse = function (id, token, options) {
options.data?.choices?.every((choice) => {
if (!bot.utils.validateLength(choice.name, { min: 1, max: 100 })) {
throw new Error(
"Invalid application command option choice name. Must be between 1-100 characters long.",
);
}
if (
typeof choice.value === "string" && (choice.value.length < 1 ||
choice.value.length > 100)
) {
throw new Error("Invalid slash options choice value type.");
}
});
return sendInteractionResponseOld(id, token, options);
};
}
export default function setupInteractionPermChecks(bot: BotWithCache) {
setupInteractionCommandPermChecks(bot);
editFollowupMessage(bot);
}

View File

@@ -0,0 +1,47 @@
import { BotWithCache } from "../deps.ts";
import { requireBotChannelPermissions } from "./permissions.ts";
export function createInvite(bot: BotWithCache) {
const createInviteOld = bot.helpers.createInvite;
bot.helpers.createInvite = function (channelId, options = {}) {
if (options.maxAge && (options.maxAge < 0 || options.maxAge > 604800)) {
throw new Error(
"The max age for an invite must be between 0 and 604800.",
);
}
if (options.maxUses && (options.maxUses < 0 || options.maxUses > 100)) {
throw new Error("The max uses for an invite must be between 0 and 100.");
}
requireBotChannelPermissions(bot, channelId, ["CREATE_INSTANT_INVITE"]);
return createInviteOld(channelId, options);
};
}
export function getChannelInvites(bot: BotWithCache) {
const getChannelInvitesOld = bot.helpers.getChannelInvites;
bot.helpers.getChannelInvites = function (channelId) {
requireBotChannelPermissions(bot, channelId, ["MANAGE_CHANNELS"]);
return getChannelInvitesOld(channelId);
};
}
export function getInvites(bot: BotWithCache) {
const getInvitesOld = bot.helpers.getInvites;
bot.helpers.getInvites = function (guildId) {
requireBotChannelPermissions(bot, guildId, ["MANAGE_GUILD"]);
return getInvitesOld(guildId);
};
}
export default function setupInvitesPermChecks(bot: BotWithCache) {
createInvite(bot);
getChannelInvites(bot);
getInvites(bot);
}

View File

@@ -0,0 +1,27 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export function banMember(bot: BotWithCache) {
const banMemberOld = bot.helpers.banMember;
bot.helpers.banMember = function (guildId, id, options) {
requireBotGuildPermissions(bot, guildId, ["BAN_MEMBERS"]);
return banMemberOld(guildId, id, options);
};
}
export function unbanMember(bot: BotWithCache) {
const unbanMemberOld = bot.helpers.unbanMember;
bot.helpers.unbanMember = function (guildId, id) {
requireBotGuildPermissions(bot, guildId, ["BAN_MEMBERS"]);
return unbanMemberOld(guildId, id);
};
}
export default function setupBanPermChecks(bot: BotWithCache) {
banMember(bot);
unbanMember(bot);
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function editBotNickname(bot: BotWithCache) {
const editBotNicknameOld = bot.helpers.editBotNickname;
bot.helpers.editBotNickname = function (guildId, options) {
requireBotGuildPermissions(bot, guildId, ["CHANGE_NICKNAME"]);
return editBotNicknameOld(guildId, options);
};
}

View File

@@ -0,0 +1,22 @@
import { BotWithCache, PermissionStrings } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function editMember(bot: BotWithCache) {
const editMemberOld = bot.helpers.editMember;
bot.helpers.editMember = function (guildId, memberId, options) {
const requiredPerms: PermissionStrings[] = [];
if (options.roles) requiredPerms.push("MANAGE_ROLES");
// NULL IS ALLOWED
if (options.nick !== undefined) requiredPerms.push("MANAGE_NICKNAMES");
if (options.channelId !== undefined) requiredPerms.push("MOVE_MEMBERS");
if (options.mute !== undefined) requiredPerms.push("MUTE_MEMBERS");
if (options.deaf !== undefined) requiredPerms.push("DEAFEN_MEMBERS");
if (requiredPerms.length) {
requireBotGuildPermissions(bot, guildId, requiredPerms);
}
return editMemberOld(guildId, memberId, options);
};
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function kickMember(bot: BotWithCache) {
const editMemberOld = bot.helpers.kickMember;
bot.helpers.kickMember = function (guildId, memberId, reason) {
requireBotGuildPermissions(bot, guildId, ["KICK_MEMBERS"]);
return editMemberOld(guildId, memberId, reason);
};
}

View File

@@ -0,0 +1,14 @@
import { BotWithCache } from "../../deps.ts";
import setupBanPermChecks from "./ban.ts";
import editBotNickname from "./editBot.ts";
import editMember from "./editMember.ts";
import kickMember from "./kickMember.ts";
import pruneMembers from "./pruneMembers.ts";
export default function setupMemberPermChecks(bot: BotWithCache) {
setupBanPermChecks(bot);
editBotNickname(bot);
editMember(bot);
kickMember(bot);
pruneMembers(bot);
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function pruneMembers(bot: BotWithCache) {
const pruneMembersOld = bot.helpers.pruneMembers;
bot.helpers.pruneMembers = function (guildId, options) {
requireBotGuildPermissions(bot, guildId, ["KICK_MEMBERS"]);
return pruneMembersOld(guildId, options);
};
}

View File

@@ -0,0 +1,200 @@
import {
AllowedMentionsTypes,
BotWithCache,
ChannelTypes,
PermissionStrings,
} from "../../deps.ts";
import { validateComponents } from "../components.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export function sendMessage(bot: BotWithCache) {
const sendMessageOld = bot.helpers.sendMessage;
bot.helpers.sendMessage = function (
channelId,
content,
) {
if (typeof content === "string") {
throw new Error("TODO");
}
const channel = bot.channels.get(channelId);
if (
channel &&
[
ChannelTypes.GuildCategory,
ChannelTypes.GuildStore,
ChannelTypes.GuildStageVoice,
].includes(channel.type)
) {
throw new Error(
`Can not send message to a channel of this type. Channel ID: ${channelId}`,
);
}
if (
content.content &&
!bot.utils.validateLength(content.content, { max: 2000 })
) {
throw new Error("The content should not exceed 2000 characters.");
}
if (content.allowedMentions) {
if (content.allowedMentions.users?.length) {
if (
content.allowedMentions.parse?.includes(
AllowedMentionsTypes.UserMentions,
)
) {
content.allowedMentions.parse = content.allowedMentions.parse.filter((
p,
) => p !== "users");
}
if (content.allowedMentions.users.length > 100) {
content.allowedMentions.users = content.allowedMentions.users.slice(
0,
100,
);
}
}
if (content.allowedMentions.roles?.length) {
if (
content.allowedMentions.parse?.includes(
AllowedMentionsTypes.RoleMentions,
)
) {
content.allowedMentions.parse = content.allowedMentions.parse.filter((
p,
) => p !== "roles");
}
if (content.allowedMentions.roles.length > 100) {
content.allowedMentions.roles = content.allowedMentions.roles.slice(
0,
100,
);
}
}
}
if (content.components) {
validateComponents(bot, content.components);
}
if (channel) {
const requiredPerms: PermissionStrings[] = [];
if (channel.guildId) {
requiredPerms.push("SEND_MESSAGES");
}
if (content.tts) requiredPerms.push("SEND_TTS_MESSAGES");
if (content.messageReference) requiredPerms.push("READ_MESSAGE_HISTORY");
if (requiredPerms.length) {
requireBotChannelPermissions(bot, channel, requiredPerms);
}
}
return sendMessageOld(channelId, content);
};
}
export function editMessage(bot: BotWithCache) {
const editMessageOld = bot.helpers.editMessage;
bot.helpers.editMessage = function (
channelId,
messageId,
content,
) {
if (typeof content === "string") {
throw new Error("TODO");
}
const message = bot.messages.get(messageId);
if (message) {
if (message.authorId !== bot.id) {
content = { flags: content.flags };
requireBotChannelPermissions(bot, channelId, ["MANAGE_MESSAGES"]);
}
}
if (content.allowedMentions) {
if (content.allowedMentions.users?.length) {
if (
content.allowedMentions.parse?.includes(
AllowedMentionsTypes.UserMentions,
)
) {
content.allowedMentions.parse = content.allowedMentions.parse.filter((
p,
) => p !== "users");
}
if (content.allowedMentions.users.length > 100) {
content.allowedMentions.users = content.allowedMentions.users.slice(
0,
100,
);
}
}
if (content.allowedMentions.roles?.length) {
if (
content.allowedMentions.parse?.includes(
AllowedMentionsTypes.RoleMentions,
)
) {
content.allowedMentions.parse = content.allowedMentions.parse.filter((
p,
) => p !== "roles");
}
if (content.allowedMentions.roles.length > 100) {
content.allowedMentions.roles = content.allowedMentions.roles.slice(
0,
100,
);
}
}
}
content.embeds?.splice(10);
if (
content.content &&
bot.utils.validateLength(content.content, { max: 2000 })
) {
throw new Error(
"A message content can not contain more than 2000 characters.",
);
}
return editMessageOld(channelId, messageId, content);
};
}
export function publishMessage(bot: BotWithCache) {
const publishMessageOld = bot.helpers.publishMessage;
bot.helpers.publishMessage = function (
channelId,
messageId,
) {
const message = bot.messages.get(messageId);
requireBotChannelPermissions(
bot,
channelId,
message?.authorId === bot.id ? ["SEND_MESSAGES"] : ["MANAGE_MESSAGES"],
);
return publishMessageOld(channelId, messageId);
};
}
export default function setupCreateMessagePermChecks(bot: BotWithCache) {
sendMessage(bot);
editMessage(bot);
publishMessage(bot);
}

View File

@@ -0,0 +1,80 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export function deleteMessage(bot: BotWithCache) {
const deleteMessageOld = bot.helpers.deleteMessage;
bot.helpers.deleteMessage = function (
channelId,
messageId,
reason,
milliseconds,
) {
const message = bot.messages.get(messageId);
// DELETING SELF MESSAGES IS ALWAYS ALLOWED
if (message?.authorId === bot.id) {
return deleteMessageOld(channelId, messageId, reason, milliseconds);
}
const channel = bot.channels.get(channelId);
if (channel?.guildId) {
requireBotChannelPermissions(bot, channel, [
"MANAGE_MESSAGES",
]);
} else {
throw new Error(
`You can only delete messages in a channel which has a guild id. Channel ID: ${channelId} Message Id: ${messageId}`,
);
}
return deleteMessageOld(channelId, messageId, reason, milliseconds);
};
}
export function deleteMessages(bot: BotWithCache) {
const deleteMessagesOld = bot.helpers.deleteMessages;
bot.helpers.deleteMessages = function (
channelId,
ids,
reason,
) {
const channel = bot.channels.get(channelId);
if (!channel?.guildId) {
throw new Error(
`Bulk deleting messages is only allowed in channels which has a guild id. Channel ID: ${channelId} IDS: ${
ids.join(" ")
}`,
);
}
// 2 WEEKS
const oldestAllowed = Date.now() - 1209600000;
ids = ids.filter((id) => {
const createdAt = Number(id / 4194304n + 1420070400000n);
// IF MESSAGE IS OLDER THAN 2 WEEKS
if (createdAt > oldestAllowed) return true;
console.log(
`[Permission Plugin] Skipping bulk message delete of ID ${id} because it is older than 2 weeks.`,
);
return false;
});
if (ids.length < 2) {
throw new Error("Bulk message delete requires at least 2 messages.");
}
requireBotChannelPermissions(bot, channel, [
"MANAGE_MESSAGES",
]);
return deleteMessagesOld(channelId, ids, reason);
};
}
export default function setupDeleteMessagePermChecks(bot: BotWithCache) {
deleteMessage(bot);
deleteMessages(bot);
}

View File

@@ -0,0 +1,44 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export function getMessage(bot: BotWithCache) {
const getMessageOld = bot.helpers.getMessage;
bot.helpers.getMessage = function (
channelId,
messageId,
) {
const channel = bot.channels.get(channelId);
if (channel?.guildId) {
requireBotChannelPermissions(bot, channel, [
"READ_MESSAGE_HISTORY",
]);
}
return getMessageOld(channelId, messageId);
};
}
export function getMessages(bot: BotWithCache) {
const getMessagesOld = bot.helpers.getMessages;
bot.helpers.getMessages = function (
channelId,
options,
) {
const channel = bot.channels.get(channelId);
if (channel?.guildId) {
requireBotChannelPermissions(bot, channel, [
"READ_MESSAGE_HISTORY",
"VIEW_CHANNEL",
]);
}
return getMessagesOld(channelId, options);
};
}
export default function setupGetMessagePermChecks(bot: BotWithCache) {
getMessage(bot);
getMessages(bot);
}

View File

@@ -0,0 +1,14 @@
import { BotWithCache } from "../../deps.ts";
import setupCreateMessagePermChecks from "./create.ts";
import setupDeleteMessagePermChecks from "./delete.ts";
import setupGetMessagePermChecks from "./get.ts";
import setupPinMessagePermChecks from "./pin.ts";
import setupReactionsPermChecks from "./reactions.ts";
export default function setupMessagesPermChecks(bot: BotWithCache) {
setupReactionsPermChecks(bot);
setupDeleteMessagePermChecks(bot);
setupGetMessagePermChecks(bot);
setupPinMessagePermChecks(bot);
setupCreateMessagePermChecks(bot);
}

View File

@@ -0,0 +1,37 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export function pinMessage(bot: BotWithCache) {
const pinMessageOld = bot.helpers.pinMessage;
bot.helpers.pinMessage = function (
channelId,
messageId,
) {
requireBotChannelPermissions(bot, channelId, [
"MANAGE_MESSAGES",
]);
return pinMessageOld(channelId, messageId);
};
}
export function unpinMessage(bot: BotWithCache) {
const unpinMessageOld = bot.helpers.unpinMessage;
bot.helpers.unpinMessage = function (
channelId,
messageId,
) {
requireBotChannelPermissions(bot, channelId, [
"MANAGE_MESSAGES",
]);
return unpinMessageOld(channelId, messageId);
};
}
export default function setupPinMessagePermChecks(bot: BotWithCache) {
pinMessage(bot);
unpinMessage(bot);
}

View File

@@ -0,0 +1,92 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export function addReaction(bot: BotWithCache) {
const addReactionOld = bot.helpers.addReaction;
bot.helpers.addReaction = function (channelId, messageId, reaction) {
requireBotChannelPermissions(bot, channelId, [
"READ_MESSAGE_HISTORY",
"ADD_REACTIONS",
]);
return addReactionOld(channelId, messageId, reaction);
};
}
export function addReactions(bot: BotWithCache) {
const addReactionsOld = bot.helpers.addReactions;
bot.helpers.addReactions = function (
channelId,
messageId,
reactions,
ordered,
) {
requireBotChannelPermissions(bot, channelId, [
"READ_MESSAGE_HISTORY",
"ADD_REACTIONS",
]);
return addReactionsOld(channelId, messageId, reactions, ordered);
};
}
export function removeReaction(bot: BotWithCache) {
const removeReactionOld = bot.helpers.removeReaction;
bot.helpers.removeReaction = function (
channelId,
messageId,
reactions,
options,
) {
// IF REMOVING OTHER USER PERMS MANAGE MESSAGES IS REQUIRED
if (options?.userId) {
requireBotChannelPermissions(bot, channelId, [
"MANAGE_MESSAGES",
]);
}
return removeReactionOld(channelId, messageId, reactions, options);
};
}
export function removeAllReactions(bot: BotWithCache) {
const removeAllReactionsOld = bot.helpers.removeAllReactions;
bot.helpers.removeAllReactions = function (
channelId,
messageId,
) {
requireBotChannelPermissions(bot, channelId, [
"MANAGE_MESSAGES",
]);
return removeAllReactionsOld(channelId, messageId);
};
}
export function removeReactionEmoji(bot: BotWithCache) {
const removeReactionEmojiOld = bot.helpers.removeReactionEmoji;
bot.helpers.removeReactionEmoji = function (
channelId,
messageId,
reaction,
) {
requireBotChannelPermissions(bot, channelId, [
"MANAGE_MESSAGES",
]);
return removeReactionEmojiOld(channelId, messageId, reaction);
};
}
export default function setupReactionsPermChecks(bot: BotWithCache) {
addReaction(bot);
addReactions(bot);
removeReaction(bot);
removeAllReactions(bot);
removeReactionEmoji(bot);
}

View File

@@ -0,0 +1,45 @@
import { BotWithCache } from "../../deps.ts";
export function editBotProfile(bot: BotWithCache) {
const editBotProfileOld = bot.helpers.editBotProfile;
bot.helpers.editBotProfile = function (
options,
) {
// Nothing was edited
if (!options.username && options.botAvatarURL === undefined) {
throw new Error(
"There was no change to the username or avatar found in the request.",
);
}
// Check username requirements if username was provided
if (options.username) {
if (options.username.length > 32) {
throw new Error(
"A username for the bot must be less than 32 characters.",
);
}
if (options.username.length < 2) {
throw new Error(
"A username for the bot can not be less than 2 characters.",
);
}
if (
["@", "#", ":", "```"].some((char) => options.username!.includes(char))
) {
throw new Error("A bot username can not include @ # : or ```");
}
if (["discordtag", "everyone", "here"].includes(options.username)) {
throw new Error(
"A bot username can not be set to `discordtag` `everyone` and `here`",
);
}
}
return editBotProfileOld(options);
};
}
export default function setupMiscPermChecks(bot: BotWithCache) {
editBotProfile(bot);
}

View File

@@ -0,0 +1,450 @@
import {
BitwisePermissionFlags,
BotWithCache,
DiscordenoChannel,
DiscordenoGuild,
DiscordenoMember,
DiscordenoRole,
Errors,
Overwrite,
PermissionStrings,
separateOverwrites,
} from "../deps.ts";
/** Calculates the permissions this member has in the given guild */
export function calculateBasePermissions(
bot: BotWithCache,
guildOrId: bigint | DiscordenoGuild,
memberOrId: bigint | DiscordenoMember,
) {
const guild = typeof guildOrId === "bigint"
? bot.guilds.get(guildOrId)
: guildOrId;
const member = typeof memberOrId === "bigint"
? bot.members.get(memberOrId)
: memberOrId;
if (!guild || !member) return 8n;
let permissions = 0n;
// Calculate the role permissions bits, @everyone role is not in memberRoleIds so we need to pass guildId manualy
permissions |= [...member.roles, guild.id]
.map((id) => guild.roles.get(id)?.permissions)
// Removes any edge case undefined
.filter((perm) => perm)
.reduce((bits, perms) => {
bits! |= perms!;
return bits;
}, 0n) || 0n;
// 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;
}
/** Calculates the permissions this member has for the given Channel */
export function calculateChannelOverwrites(
bot: BotWithCache,
channelOrId: bigint | DiscordenoChannel,
memberOrId: bigint | DiscordenoMember,
) {
const channel = typeof channelOrId === "bigint"
? bot.channels.get(channelOrId)
: channelOrId;
// This is a DM channel so return ADMINISTRATOR permission
if (!channel?.guildId) return 8n;
const member = typeof memberOrId === "bigint"
? bot.members.get(memberOrId)
: memberOrId;
if (!channel || !member) return 8n;
// Get all the role permissions this member already has
let permissions = calculateBasePermissions(
bot,
channel.guildId,
member,
);
// First calculate @everyone overwrites since these have the lowest priority
const overwriteEveryone = channel.permissionOverwrites?.find((overwrite) => {
const [_, id] = separateOverwrites(overwrite);
return id === channel.guildId;
});
if (overwriteEveryone) {
const [_type, _id, allow, deny] = separateOverwrites(overwriteEveryone);
// First remove denied permissions since denied < allowed
permissions &= ~deny;
permissions |= 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.roles || [];
// Second calculate members role overwrites since these have middle priority
for (const overwrite of overwrites || []) {
const [_type, id, allowBits, denyBits] = separateOverwrites(overwrite);
if (!memberRoles.includes(id)) continue;
deny |= denyBits;
allow |= allowBits;
}
// 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) => {
const [_, id] = separateOverwrites(overwrite);
return id === member.id;
});
if (overwriteMember) {
const [_type, _id, allowBits, denyBits] = separateOverwrites(
overwriteMember,
);
permissions &= ~denyBits;
permissions |= allowBits;
}
return permissions;
}
/** Checks if the given permission bits are matching the given permissions. `ADMINISTRATOR` always returns `true` */
export function validatePermissions(
permissionBits: bigint,
permissions: PermissionStrings[],
) {
if (permissionBits & 8n) return true;
return permissions.every(
(permission) =>
// Check if permission is in permissionBits
permissionBits & BigInt(BitwisePermissionFlags[permission]),
);
}
/** Checks if the given member has these permissions in the given guild */
export function hasGuildPermissions(
bot: BotWithCache,
guild: bigint | DiscordenoGuild,
member: bigint | DiscordenoMember,
permissions: PermissionStrings[],
) {
// First we need the role permission bits this member has
const basePermissions = calculateBasePermissions(
bot,
guild,
member,
);
// Second use the validatePermissions function to check if the member has every permission
return validatePermissions(basePermissions, permissions);
}
/** Checks if the bot has these permissions in the given guild */
export function botHasGuildPermissions(
bot: BotWithCache,
guild: bigint | DiscordenoGuild,
permissions: PermissionStrings[],
) {
// Since Bot is a normal member we can use the hasRolePermissions() function
return hasGuildPermissions(bot, guild, bot.id, permissions);
}
/** Checks if the given member has these permissions for the given channel */
export function hasChannelPermissions(
bot: BotWithCache,
channel: bigint | DiscordenoChannel,
member: bigint | DiscordenoMember,
permissions: PermissionStrings[],
) {
// First we need the overwrite bits this member has
const channelOverwrites = calculateChannelOverwrites(
bot,
channel,
member,
);
// Second use the validatePermissions function to check if the member has every permission
return validatePermissions(channelOverwrites, permissions);
}
/** Checks if the bot has these permissions f0r the given channel */
export function botHasChannelPermissions(
bot: BotWithCache,
channel: bigint | DiscordenoChannel,
permissions: PermissionStrings[],
) {
// Since Bot is a normal member we can use the hasRolePermissions() function
return hasChannelPermissions(bot, channel, bot.id, permissions);
}
/** Returns the permissions that are not in the given permissionBits */
export function missingPermissions(
permissionBits: bigint,
permissions: PermissionStrings[],
) {
if (permissionBits & 8n) return [];
return permissions.filter((permission) =>
!(permissionBits & BigInt(BitwisePermissionFlags[permission]))
);
}
/** Get the missing Guild permissions this member has */
export function getMissingGuildPermissions(
bot: BotWithCache,
guild: bigint | DiscordenoGuild,
member: bigint | DiscordenoMember,
permissions: PermissionStrings[],
) {
// First we need the role permission bits this member has
const permissionBits = calculateBasePermissions(
bot,
guild,
member,
);
// Second return the members missing permissions
return missingPermissions(permissionBits, permissions);
}
/** Get the missing Channel permissions this member has */
export function getMissingChannelPermissions(
bot: BotWithCache,
channel: bigint | DiscordenoChannel,
member: bigint | DiscordenoMember,
permissions: PermissionStrings[],
) {
// First we need the role permissino bits this member has
const permissionBits = calculateChannelOverwrites(
bot,
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 function requireGuildPermissions(
bot: BotWithCache,
guild: bigint | DiscordenoGuild,
member: bigint | DiscordenoMember,
permissions: PermissionStrings[],
) {
const missing = getMissingGuildPermissions(
bot,
guild,
member,
permissions,
);
if (missing.length) {
// If the member is missing a permission throw an Error
throw new Error(`Missing Permissions: ${missing.join(" & ")}`);
}
}
/** Throws an error if the bot does not have all permissions */
export function requireBotGuildPermissions(
bot: BotWithCache,
guild: bigint | DiscordenoGuild,
permissions: PermissionStrings[],
) {
// Since Bot is a normal member we can use the throwOnMissingGuildPermission() function
return requireGuildPermissions(bot, guild, bot.id, permissions);
}
/** Throws an error if this member has not all of the given permissions */
export function requireChannelPermissions(
bot: BotWithCache,
channel: bigint | DiscordenoChannel,
member: bigint | DiscordenoMember,
permissions: PermissionStrings[],
) {
const missing = getMissingChannelPermissions(
bot,
channel,
member,
permissions,
);
if (missing.length) {
// If the member is missing a permission throw an Error
throw new Error(`Missing Permissions: ${missing.join(" & ")}`);
}
}
/** Throws an error if the bot has not all of the given channel permissions */
export function requireBotChannelPermissions(
bot: BotWithCache,
channel: bigint | DiscordenoChannel,
permissions: PermissionStrings[],
) {
// Since Bot is a normal member we can use the throwOnMissingChannelPermission() function
return requireChannelPermissions(bot, channel, bot.id, permissions);
}
/** This function converts a bitwise string to permission strings */
export function calculatePermissions(permissionBits: bigint) {
return Object.keys(BitwisePermissionFlags).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(BitwisePermissionFlags[permission as PermissionStrings]);
}) as PermissionStrings[];
}
/** This function converts an array of permissions into the bitwise string. */
export function calculateBits(permissions: PermissionStrings[]) {
return permissions
.reduce((bits, perm) => {
bits |= BigInt(BitwisePermissionFlags[perm]);
return bits;
}, 0n)
.toString();
}
/** Internal function to check if the bot has the permissions to set these overwrites */
export function requireOverwritePermissions(
bot: BotWithCache,
guildOrId: bigint | DiscordenoGuild,
overwrites: Overwrite[],
) {
let requiredPerms: Set<PermissionStrings> = new Set(["MANAGE_CHANNELS"]);
overwrites?.forEach((overwrite) => {
if (overwrite.allow) {
overwrite.allow.forEach(requiredPerms.add, requiredPerms);
}
if (overwrite.deny) {
overwrite.deny.forEach(requiredPerms.add, requiredPerms);
}
});
// MANAGE_ROLES permission can only be set by administrators
if (requiredPerms.has("MANAGE_ROLES")) {
requiredPerms = new Set<PermissionStrings>(["ADMINISTRATOR"]);
}
requireGuildPermissions(bot, guildOrId, bot.id, [
...requiredPerms,
]);
}
/** Gets the highest role from the member in this guild */
export function highestRole(
bot: BotWithCache,
guildOrId: bigint | DiscordenoGuild,
memberOrId: bigint | DiscordenoMember,
) {
const guild = typeof guildOrId === "bigint"
? bot.guilds.get(guildOrId)
: guildOrId;
if (!guild) throw new Error(Errors.GUILD_NOT_FOUND);
// Get the roles from the member
const memberRoles =
(typeof memberOrId === "bigint" ? bot.members.get(memberOrId) : memberOrId)
?.roles;
// This member has no roles so the highest one is the @everyone role
if (!memberRoles) return guild.roles.get(guild.id)!;
let memberHighestRole: DiscordenoRole | undefined;
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.position === role.position
) {
memberHighestRole = 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 function higherRolePosition(
bot: BotWithCache,
guildOrId: bigint | DiscordenoGuild,
roleId: bigint,
otherRoleId: bigint,
) {
const guild = typeof guildOrId === "bigint" ? bot.guilds.get(guildOrId) : guildOrId;
if (!guild) return true;
const role = guild.roles.get(roleId);
const otherRole = guild.roles.get(otherRoleId);
if (!role || !otherRole) throw new Error(Errors.ROLE_NOT_FOUND);
// Rare edge case handling
if (role.position === otherRole.position) {
return role.id < otherRole.id;
}
return role.position > otherRole.position;
}
/** Checks if the member has a higher position than the given role */
export function isHigherPosition(
bot: BotWithCache,
guildOrId: bigint | DiscordenoGuild,
memberId: bigint,
compareRoleId: bigint,
) {
const guild = typeof guildOrId === "bigint" ? bot.guilds.get(guildOrId) : guildOrId;
if (!guild || guild.ownerId === memberId) return true;
const memberHighestRole = highestRole(bot, guild, memberId);
return higherRolePosition(
bot,
guild.id,
memberHighestRole.id,
compareRoleId,
);
}
/** Checks if a channel overwrite for a user id or a role id has permission in this channel */
export function channelOverwriteHasPermission(
guildId: bigint,
id: bigint,
overwrites: bigint[],
permissions: PermissionStrings[]
) {
const overwrite =
overwrites.find((perm) => {
const [_, bitID] = separateOverwrites(perm);
return id === bitID;
}) ||
overwrites.find((perm) => {
const [_, bitID] = separateOverwrites(perm);
return bitID === guildId;
});
if (!overwrite) return false;
return permissions.every((perm) => {
const [_type, _id, allowBits, denyBits] = separateOverwrites(overwrite);
if (BigInt(denyBits) & BigInt(BitwisePermissionFlags[perm])) {
return false;
}
if (BigInt(allowBits) & BigInt(BitwisePermissionFlags[perm])) {
return true;
}
});
}

View File

@@ -0,0 +1,32 @@
import { BotWithCache } from "../../deps.ts";
import { higherRolePosition } from "../permissions.ts";
import { highestRole, requireBotGuildPermissions } from "../permissions.ts";
export default function addRole(bot: BotWithCache) {
const addRoleOld = bot.helpers.addRole;
bot.helpers.addRole = function (
guildId,
memberId,
roleId,
reason,
) {
const guild = bot.guilds.get(guildId);
if (guild) {
const role = guild.roles.get(roleId);
if (role) {
const botRole = highestRole(bot, guild, bot.id);
if (!higherRolePosition(bot, guild, botRole.id, role.id)) {
throw new Error(
`The bot can not add this role to the member because it does not have a role higher than the role ID: ${role.id}.`,
);
}
}
requireBotGuildPermissions(bot, guild, ["MANAGE_ROLES"]);
}
return addRoleOld(guildId, memberId, roleId, reason);
};
}

View File

@@ -0,0 +1,16 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function createRole(bot: BotWithCache) {
const createRoleOld = bot.helpers.createRole;
bot.helpers.createRole = function (
guildId,
options,
reason,
) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_ROLES"]);
return createRoleOld(guildId, options, reason);
};
}

View File

@@ -0,0 +1,15 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotGuildPermissions } from "../permissions.ts";
export default function deleteRole(bot: BotWithCache) {
const deleteRoleOld = bot.helpers.deleteRole;
bot.helpers.deleteRole = function (
guildId,
id,
) {
requireBotGuildPermissions(bot, guildId, ["MANAGE_ROLES"]);
return deleteRoleOld(guildId, id);
};
}

View File

@@ -0,0 +1,31 @@
import { BotWithCache } from "../../deps.ts";
import { higherRolePosition } from "../permissions.ts";
import { highestRole, requireBotGuildPermissions } from "../permissions.ts";
export default function editRole(bot: BotWithCache) {
const editRoleOld = bot.helpers.editRole;
bot.helpers.editRole = function (
guildId,
id,
options,
) {
const guild = bot.guilds.get(guildId);
if (guild) {
const role = guild.roles.get(id);
if (role) {
const botRole = highestRole(bot, guild, bot.id);
if (!higherRolePosition(bot, guild, botRole.id, role.id)) {
throw new Error(
`The bot can not add this role to the member because it does not have a role higher than the role ID: ${role.id}.`,
);
}
}
requireBotGuildPermissions(bot, guild, ["MANAGE_ROLES"]);
}
return editRoleOld(guildId, id, options);
};
}

View File

@@ -0,0 +1,14 @@
import { BotWithCache } from "../../deps.ts";
import addRole from "./add.ts";
import createRole from "./create.ts";
import deleteRole from "./delete.ts";
import editRole from "./edit.ts";
import removeRole from "./remove.ts";
export default function setupRolePermChecks(bot: BotWithCache) {
addRole(bot);
createRole(bot);
deleteRole(bot);
editRole(bot);
removeRole(bot);
}

View File

@@ -0,0 +1,32 @@
import { BotWithCache } from "../../deps.ts";
import { higherRolePosition } from "../permissions.ts";
import { highestRole, requireBotGuildPermissions } from "../permissions.ts";
export default function removeRole(bot: BotWithCache) {
const removeRoleOld = bot.helpers.removeRole;
bot.helpers.removeRole = function (
guildId,
memberId,
roleId,
reason,
) {
const guild = bot.guilds.get(guildId);
if (guild) {
const role = guild.roles.get(roleId);
if (role) {
const botRole = highestRole(bot, guild, bot.id);
if (!higherRolePosition(bot, guild, botRole.id, role.id)) {
throw new Error(
`The bot can not add this role to the member because it does not have a role higher than the role ID: ${role.id}.`,
);
}
}
requireBotGuildPermissions(bot, guild, ["MANAGE_ROLES"]);
}
return removeRoleOld(guildId, memberId, roleId, reason);
};
}

View File

@@ -0,0 +1,22 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export default function createWebhook(bot: BotWithCache) {
const createWebhookOld = bot.helpers.createWebhook;
bot.helpers.createWebhook = function (channelId, options) {
requireBotChannelPermissions(bot, channelId, ["MANAGE_WEBHOOKS"]);
if (
// Specific usernames that discord does not allow
options.name === "clyde" ||
!bot.utils.validateLength(options.name, { min: 2, max: 32 })
) {
throw new Error(
"The webhook name can not be clyde and it must be between 2 and 32 characters long.",
);
}
return createWebhookOld(channelId, options);
};
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export default function deleteWebhook(bot: BotWithCache) {
const deleteWebhookOld = bot.helpers.deleteWebhook;
bot.helpers.deleteWebhook = function (channelId, options) {
requireBotChannelPermissions(bot, channelId, ["MANAGE_WEBHOOKS"]);
return deleteWebhookOld(channelId, options);
};
}

View File

@@ -0,0 +1,23 @@
import { BotWithCache } from "../../deps.ts";
import { requireBotChannelPermissions } from "../permissions.ts";
export default function editWebhook(bot: BotWithCache) {
const editWebhookOld = bot.helpers.editWebhook;
bot.helpers.editWebhook = function (channelId, webhookId, options) {
requireBotChannelPermissions(bot, channelId, ["MANAGE_WEBHOOKS"]);
if (options.name) {
if (
// Specific usernames that discord does not allow
options.name === "clyde" ||
!bot.utils.validateLength(options.name, { min: 2, max: 32 })
) {
throw new Error(
"The webhook name can not be clyde and it must be between 2 and 32 characters long.",
);
}
}
return editWebhookOld(channelId, webhookId, options);
};
}

View File

@@ -0,0 +1,71 @@
import { AllowedMentionsTypes, BotWithCache } from "../../deps.ts";
import { validateComponents } from "../components.ts";
export function editWebhookMessage(bot: BotWithCache) {
const editWebhookMessageOld = bot.helpers.editWebhookMessage;
bot.helpers.editWebhookMessage = function (
webhookId,
webhookToken,
options,
) {
if (
options.content &&
!bot.utils.validateLength(options.content, { max: 2000 })
) {
throw Error("The content can not exceed 2000 characters.");
}
if (options.embeds && options.embeds.length > 10) {
options.embeds.splice(10);
}
if (options.allowedMentions) {
if (options.allowedMentions.users?.length) {
if (
options.allowedMentions.parse?.includes(
AllowedMentionsTypes.UserMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((
p,
) => p !== "users");
}
if (options.allowedMentions.users.length > 100) {
options.allowedMentions.users = options.allowedMentions.users.slice(
0,
100,
);
}
}
if (options.allowedMentions.roles?.length) {
if (
options.allowedMentions.parse?.includes(
AllowedMentionsTypes.RoleMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((
p,
) => p !== "roles");
}
if (options.allowedMentions.roles.length > 100) {
options.allowedMentions.roles = options.allowedMentions.roles.slice(
0,
100,
);
}
}
}
if (options.components) validateComponents(bot, options.components);
return editWebhookMessageOld(webhookId, webhookToken, options);
};
}
export default function setupMessageWebhookPermChecks(bot: BotWithCache) {
editWebhookMessage(bot);
}

View File

@@ -0,0 +1,12 @@
import { BotWithCache } from "../../deps.ts";
import createWebhook from "./createWebhook.ts";
import deleteWebhook from "./deleteWebhook.ts";
import editWebhook from "./editWebhook.ts";
import setupMessageWebhookPermChecks from "./message.ts";
export default function setupWebhooksPermChecks(bot: BotWithCache) {
createWebhook(bot);
deleteWebhook(bot);
editWebhook(bot);
setupMessageWebhookPermChecks(bot);
}

View File

@@ -0,0 +1,67 @@
import { AllowedMentionsTypes, BotWithCache } from "../../deps.ts";
import { validateComponents } from "../components.ts";
export default function sendWebhook(bot: BotWithCache) {
const sendWebhookOld = bot.helpers.sendWebhook;
bot.helpers.sendWebhook = function (webhookId, webhookToken, options) {
if (
options.content &&
!bot.utils.validateLength(options.content, { max: 2000 })
) {
throw new Error("The content should not exceed 2000 characters.");
}
if (options.allowedMentions) {
if (options.allowedMentions.users?.length) {
if (
options.allowedMentions.parse?.includes(
AllowedMentionsTypes.UserMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((
p,
) => p !== "users");
}
if (options.allowedMentions.users.length > 100) {
options.allowedMentions.users = options.allowedMentions.users.slice(
0,
100,
);
}
}
if (options.allowedMentions.roles?.length) {
if (
options.allowedMentions.parse?.includes(
AllowedMentionsTypes.RoleMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((
p,
) => p !== "roles");
}
if (options.allowedMentions.roles.length > 100) {
options.allowedMentions.roles = options.allowedMentions.roles.slice(
0,
100,
);
}
}
}
if (options.components) {
validateComponents(bot, options.components);
}
if (!options.content && !options.file && !options.embeds) {
throw new Error(
"You must provide a value for at least one of content, embeds, or file.",
);
}
return sendWebhookOld(webhookId, webhookToken, options);
};
}