mirror of
https://github.com/discordeno/discordeno.git
synced 2026-06-01 16:30:08 +00:00
Merge branch 'discordeno:main' into 1898
This commit is contained in:
21
plugins/cache/README.md
vendored
Normal file
21
plugins/cache/README.md
vendored
Normal 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
1
plugins/cache/deps.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "../../mod.ts";
|
||||
159
plugins/cache/mod.ts
vendored
Normal file
159
plugins/cache/mod.ts
vendored
Normal 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";
|
||||
39
plugins/cache/src/addCacheCollections.ts
vendored
Normal file
39
plugins/cache/src/addCacheCollections.ts
vendored
Normal 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>;
|
||||
}
|
||||
99
plugins/cache/src/dispatchRequirements.ts
vendored
Normal file
99
plugins/cache/src/dispatchRequirements.ts
vendored
Normal 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
120
plugins/cache/src/setupCacheEdits.ts
vendored
Normal 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
124
plugins/cache/src/setupCacheRemovals.ts
vendored
Normal 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
76
plugins/cache/src/sweepers.ts
vendored
Normal 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);
|
||||
};
|
||||
}
|
||||
32
plugins/fileloader/README.md
Normal file
32
plugins/fileloader/README.md
Normal 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.
|
||||
1
plugins/fileloader/deps.ts
Normal file
1
plugins/fileloader/deps.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "../../mod.ts";
|
||||
99
plugins/fileloader/mod.ts
Normal file
99
plugins/fileloader/mod.ts
Normal 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;
|
||||
}
|
||||
1
plugins/helpers/README.md
Normal file
1
plugins/helpers/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# helpers-plugin
|
||||
1
plugins/helpers/deps.ts
Normal file
1
plugins/helpers/deps.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "../../mod.ts";
|
||||
123
plugins/helpers/mod.ts
Normal file
123
plugins/helpers/mod.ts
Normal 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;
|
||||
46
plugins/helpers/src/channels.ts
Normal file
46
plugins/helpers/src/channels.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
6
plugins/helpers/src/disconnectMember.ts
Normal file
6
plugins/helpers/src/disconnectMember.ts
Normal 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 });
|
||||
}
|
||||
73
plugins/helpers/src/getMembersPaginated.ts
Normal file
73
plugins/helpers/src/getMembersPaginated.ts
Normal 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;
|
||||
}
|
||||
13
plugins/helpers/src/moveMember.ts
Normal file
13
plugins/helpers/src/moveMember.ts
Normal 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 });
|
||||
}
|
||||
19
plugins/helpers/src/sendAutoCompleteChoices.ts
Normal file
19
plugins/helpers/src/sendAutoCompleteChoices.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
27
plugins/helpers/src/sendDirectMessage.ts
Normal file
27
plugins/helpers/src/sendDirectMessage.ts
Normal 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);
|
||||
}
|
||||
17
plugins/helpers/src/suppressEmbeds.ts
Normal file
17
plugins/helpers/src/suppressEmbeds.ts
Normal 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);
|
||||
}
|
||||
38
plugins/helpers/src/threads.ts
Normal file
38
plugins/helpers/src/threads.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
25
plugins/permissions/README.md
Normal file
25
plugins/permissions/README.md
Normal 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);
|
||||
```
|
||||
|
||||
2
plugins/permissions/deps.ts
Normal file
2
plugins/permissions/deps.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "../../mod.ts";
|
||||
export type { BotWithCache } from "../cache/mod.ts";
|
||||
48
plugins/permissions/mod.ts
Normal file
48
plugins/permissions/mod.ts
Normal 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;
|
||||
37
plugins/permissions/src/channels/deleteChannel.ts
Normal file
37
plugins/permissions/src/channels/deleteChannel.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
16
plugins/permissions/src/channels/deleteChannelOverwrite.ts
Normal file
16
plugins/permissions/src/channels/deleteChannelOverwrite.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
123
plugins/permissions/src/channels/editChannel.ts
Normal file
123
plugins/permissions/src/channels/editChannel.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
15
plugins/permissions/src/channels/editChannelOverwrite.ts
Normal file
15
plugins/permissions/src/channels/editChannelOverwrite.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
15
plugins/permissions/src/channels/followChannel.ts
Normal file
15
plugins/permissions/src/channels/followChannel.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
15
plugins/permissions/src/channels/getChannelWebhooks.ts
Normal file
15
plugins/permissions/src/channels/getChannelWebhooks.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
22
plugins/permissions/src/channels/mod.ts
Normal file
22
plugins/permissions/src/channels/mod.ts
Normal 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);
|
||||
}
|
||||
56
plugins/permissions/src/channels/stage.ts
Normal file
56
plugins/permissions/src/channels/stage.ts
Normal 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);
|
||||
}
|
||||
12
plugins/permissions/src/channels/swapChannels.ts
Normal file
12
plugins/permissions/src/channels/swapChannels.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
26
plugins/permissions/src/channels/threads/addToThread.ts
Normal file
26
plugins/permissions/src/channels/threads/addToThread.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
16
plugins/permissions/src/channels/threads/getThreadMembers.ts
Normal file
16
plugins/permissions/src/channels/threads/getThreadMembers.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
15
plugins/permissions/src/channels/threads/joinThread.ts
Normal file
15
plugins/permissions/src/channels/threads/joinThread.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
15
plugins/permissions/src/channels/threads/leaveThread.ts
Normal file
15
plugins/permissions/src/channels/threads/leaveThread.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
16
plugins/permissions/src/channels/threads/mod.ts
Normal file
16
plugins/permissions/src/channels/threads/mod.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
178
plugins/permissions/src/components.ts
Normal file
178
plugins/permissions/src/components.ts
Normal 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;
|
||||
}
|
||||
43
plugins/permissions/src/connectToVoiceChannels.ts
Normal file
43
plugins/permissions/src/connectToVoiceChannels.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
49
plugins/permissions/src/discovery.ts
Normal file
49
plugins/permissions/src/discovery.ts
Normal 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);
|
||||
}
|
||||
67
plugins/permissions/src/editMember.ts
Normal file
67
plugins/permissions/src/editMember.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
38
plugins/permissions/src/emojis.ts
Normal file
38
plugins/permissions/src/emojis.ts
Normal 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)
|
||||
}
|
||||
22
plugins/permissions/src/guilds/createGuild.ts
Normal file
22
plugins/permissions/src/guilds/createGuild.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
14
plugins/permissions/src/guilds/deleteGuild.ts
Normal file
14
plugins/permissions/src/guilds/deleteGuild.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
12
plugins/permissions/src/guilds/editGuild.ts
Normal file
12
plugins/permissions/src/guilds/editGuild.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
144
plugins/permissions/src/guilds/events.ts
Normal file
144
plugins/permissions/src/guilds/events.ts
Normal 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);
|
||||
}
|
||||
12
plugins/permissions/src/guilds/getAuditLogs.ts
Normal file
12
plugins/permissions/src/guilds/getAuditLogs.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
12
plugins/permissions/src/guilds/getBan.ts
Normal file
12
plugins/permissions/src/guilds/getBan.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
12
plugins/permissions/src/guilds/getBans.ts
Normal file
12
plugins/permissions/src/guilds/getBans.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
12
plugins/permissions/src/guilds/getPruneCount.ts
Normal file
12
plugins/permissions/src/guilds/getPruneCount.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
12
plugins/permissions/src/guilds/getVanityUrl.ts
Normal file
12
plugins/permissions/src/guilds/getVanityUrl.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
26
plugins/permissions/src/guilds/mod.ts
Normal file
26
plugins/permissions/src/guilds/mod.ts
Normal 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);
|
||||
}
|
||||
16
plugins/permissions/src/guilds/welcomeScreen.ts
Normal file
16
plugins/permissions/src/guilds/welcomeScreen.ts
Normal 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);
|
||||
}
|
||||
16
plugins/permissions/src/guilds/widget.ts
Normal file
16
plugins/permissions/src/guilds/widget.ts
Normal 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);
|
||||
}
|
||||
27
plugins/permissions/src/integrations.ts
Normal file
27
plugins/permissions/src/integrations.ts
Normal 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);
|
||||
}
|
||||
201
plugins/permissions/src/interactions/commands.ts
Normal file
201
plugins/permissions/src/interactions/commands.ts
Normal 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);
|
||||
}
|
||||
61
plugins/permissions/src/interactions/editFollowupMessage.ts
Normal file
61
plugins/permissions/src/interactions/editFollowupMessage.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
31
plugins/permissions/src/interactions/mod.ts
Normal file
31
plugins/permissions/src/interactions/mod.ts
Normal 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);
|
||||
}
|
||||
47
plugins/permissions/src/invites.ts
Normal file
47
plugins/permissions/src/invites.ts
Normal 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);
|
||||
}
|
||||
27
plugins/permissions/src/members/ban.ts
Normal file
27
plugins/permissions/src/members/ban.ts
Normal 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);
|
||||
}
|
||||
12
plugins/permissions/src/members/editBot.ts
Normal file
12
plugins/permissions/src/members/editBot.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
22
plugins/permissions/src/members/editMember.ts
Normal file
22
plugins/permissions/src/members/editMember.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
12
plugins/permissions/src/members/kickMember.ts
Normal file
12
plugins/permissions/src/members/kickMember.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
14
plugins/permissions/src/members/mod.ts
Normal file
14
plugins/permissions/src/members/mod.ts
Normal 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);
|
||||
}
|
||||
12
plugins/permissions/src/members/pruneMembers.ts
Normal file
12
plugins/permissions/src/members/pruneMembers.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
200
plugins/permissions/src/messages/create.ts
Normal file
200
plugins/permissions/src/messages/create.ts
Normal 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);
|
||||
}
|
||||
80
plugins/permissions/src/messages/delete.ts
Normal file
80
plugins/permissions/src/messages/delete.ts
Normal 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);
|
||||
}
|
||||
44
plugins/permissions/src/messages/get.ts
Normal file
44
plugins/permissions/src/messages/get.ts
Normal 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);
|
||||
}
|
||||
14
plugins/permissions/src/messages/mod.ts
Normal file
14
plugins/permissions/src/messages/mod.ts
Normal 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);
|
||||
}
|
||||
37
plugins/permissions/src/messages/pin.ts
Normal file
37
plugins/permissions/src/messages/pin.ts
Normal 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);
|
||||
}
|
||||
92
plugins/permissions/src/messages/reactions.ts
Normal file
92
plugins/permissions/src/messages/reactions.ts
Normal 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);
|
||||
}
|
||||
45
plugins/permissions/src/misc/mod.ts
Normal file
45
plugins/permissions/src/misc/mod.ts
Normal 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);
|
||||
}
|
||||
450
plugins/permissions/src/permissions.ts
Normal file
450
plugins/permissions/src/permissions.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
32
plugins/permissions/src/roles/add.ts
Normal file
32
plugins/permissions/src/roles/add.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
16
plugins/permissions/src/roles/create.ts
Normal file
16
plugins/permissions/src/roles/create.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
15
plugins/permissions/src/roles/delete.ts
Normal file
15
plugins/permissions/src/roles/delete.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
31
plugins/permissions/src/roles/edit.ts
Normal file
31
plugins/permissions/src/roles/edit.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
14
plugins/permissions/src/roles/mod.ts
Normal file
14
plugins/permissions/src/roles/mod.ts
Normal 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);
|
||||
}
|
||||
32
plugins/permissions/src/roles/remove.ts
Normal file
32
plugins/permissions/src/roles/remove.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
22
plugins/permissions/src/webhooks/createWebhook.ts
Normal file
22
plugins/permissions/src/webhooks/createWebhook.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
12
plugins/permissions/src/webhooks/deleteWebhook.ts
Normal file
12
plugins/permissions/src/webhooks/deleteWebhook.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
23
plugins/permissions/src/webhooks/editWebhook.ts
Normal file
23
plugins/permissions/src/webhooks/editWebhook.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
71
plugins/permissions/src/webhooks/message.ts
Normal file
71
plugins/permissions/src/webhooks/message.ts
Normal 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);
|
||||
}
|
||||
12
plugins/permissions/src/webhooks/mod.ts
Normal file
12
plugins/permissions/src/webhooks/mod.ts
Normal 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);
|
||||
}
|
||||
67
plugins/permissions/src/webhooks/sendWebhook.ts
Normal file
67
plugins/permissions/src/webhooks/sendWebhook.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user