diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 358558525..7d24bee7b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,9 +15,11 @@ Examples of good PR title: -- fix(controllers/interactions): cache member from INTERACTION_CREATE payload +- fix(handlers/INTERACTION_CREATE): cache member object - docs: improve wording -- feat(handlers/guild): add editGuild() function Examples of bad PR title: +- feat: add cache manager module +- feat(helpers): add editGuild() +- refactor(ws/shard): remove redundant checks Examples of bad PR title: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..91031e0af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** A clear and concise description of what the bug is. + +**To Reproduce** Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** A clear and concise description of what you expected to +happen. + +**Screenshots** If applicable, add screenshots to help explain your problem. + +**Version details (please complete the following information):** + +- Discordeno version: [e.g. 10.5.0] +- Deno version: [e.g. 1.8.0] + +**Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..75be6c60a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feat +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** A clear and +concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** A clear and concise description of what you +want to happen. + +**Describe alternatives you've considered** A clear and concise description of +any alternative solutions or features you've considered. + +**Additional context** Add any other context or screenshots about the feature +request here. diff --git a/README.md b/README.md index 27dae2dc8..ea825051f 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ libraries. If you are a beginner, you can check out these awesome official and unofficial boilerplates: - [Discordeno Boilerplate (official)](https://github.com/discordeno/boilerplate) -- [Serverless Slash Commands Template +- [Serverless Slash Commands Boilerplate (official)](https://github.com/discordeno/slash-commands-boilerplate) - [Add Your Own!](https://github.com/discordeno/discordeno/pulls) diff --git a/deps.ts b/deps.ts index 6988b6a9a..842c7c13b 100644 --- a/deps.ts +++ b/deps.ts @@ -1 +1 @@ -export { encode } from "https://deno.land/std@0.88.0/encoding/base64.ts"; +export { encode } from "https://deno.land/std@0.90.0/encoding/base64.ts"; diff --git a/docs/src/advanced/customizations.md b/docs/src/advanced/customizations.md index a8a35b385..ebc98490a 100644 --- a/docs/src/advanced/customizations.md +++ b/docs/src/advanced/customizations.md @@ -34,17 +34,17 @@ To begin customizing, create a file in the structures folder called `member.ts`. The name of the file is not important at all. ```ts -async function createMember() { +async function createMemberStruct() { } ``` We start by declaring a function that will be run to create the structure. Once again the name here is not important. The function must take the same arguments -that the internal function takes. In this case the createMember function takes 2 -arguments. `data: MemberCreatePayload, guildID: string` +that the internal function takes. In this case the createMemberStruct function +takes 2 arguments. `data: MemberCreatePayload, guildID: string` ```ts -async function createMember(data: MemberCreatePayload, guildID: string) { +async function createMemberStruct(data: MemberCreatePayload, guildID: string) { } ``` @@ -53,7 +53,7 @@ want. My recommendation is to start by copying the current code from the internal libraries structure. ```ts -async function createMember(data: MemberCreatePayload, guildID: string) { +async function createMemberStruct(data: MemberCreatePayload, guildID: string) { const { joined_at: joinedAt, premium_since: premiumSince, @@ -92,7 +92,7 @@ and `guild` properties to the member. ```ts import { rawAvatarURL } from "../../deps.ts"; -async function createMember(data: MemberCreatePayload, guildID: string) { +async function createMemberStruct(data: MemberCreatePayload, guildID: string) { // Hidden code here to make it easier to see the changes const member = { @@ -116,11 +116,11 @@ async function createMember(data: MemberCreatePayload, guildID: string) { ``` Now we need to use this function and telling Discordeno to override the internal -createMember function. To do this, we will modify the internal functions. This -is where we reassign the value of the function. +createMemberStruct function. To do this, we will modify the internal functions. +This is where we reassign the value of the function. ```ts -structures.createMember = createMember; +structures.createMemberStruct = createMemberStruct; ``` Awesome. Now, we have one more step to complete which is to declare these new @@ -144,7 +144,7 @@ declare module "../../deps.ts" { The code should look like this right now: ```ts -async function createMember(data: MemberCreatePayload, guildID: string) { +async function createMemberStruct(data: MemberCreatePayload, guildID: string) { const { joined_at: joinedAt, premium_since: premiumSince, @@ -184,7 +184,7 @@ async function createMember(data: MemberCreatePayload, guildID: string) { return member; } -structures.createMember = createMember; +structures.createMemberStruct = createMemberStruct; declare module "../../deps.ts" { interface Member { @@ -268,7 +268,7 @@ import { rawAvatarURL, } from "../../deps.ts"; -async function createMember(data: MemberCreatePayload, guildID: string) { +async function createMemberStruct(data: MemberCreatePayload, guildID: string) { const { id, bot, @@ -291,7 +291,7 @@ async function createMember(data: MemberCreatePayload, guildID: string) { }; } -structures.createMember = createMember; +structures.createMemberStruct = createMemberStruct; declare module "../../deps.ts" { interface Member { @@ -304,15 +304,15 @@ declare module "../../deps.ts" { } ``` -You might be seeing an error on `structures.createMember`. This is happening -because our new member structures is modifying/removing existing properties that -the lib internally said it would have. To solve this, simply just add a -ts-ignore above it as you know better than TS that the typings are being -overwritten. +You might be seeing an error on `structures.createMemberStruct`. This is +happening because our new member structures is modifying/removing existing +properties that the lib internally said it would have. To solve this, simply +just add a ts-ignore above it as you know better than TS that the typings are +being overwritten. ```ts // @ts-ignore -structures.createMember = createMember; +structures.createMemberStruct = createMemberStruct; ``` ## Custom Cache @@ -347,10 +347,10 @@ methods on the cacheHandlers. The current list of methods available are: - set - forEach -## Custom Gateway Payload Handling (Controllers) +## Custom Gateway Payload Handling (Handlers) -Controllers are one of the most powerful features of Discordeno. They allow you -to take control of how Discordeno handles the Discord payloads from the gateway. +Handlers are one of the most powerful features of Discordeno. They allow you to +take control of how Discordeno handles the Discord payloads from the gateway. When an event comes in, you can override and control how you want it to work. For example, if your bot does not use emojis at all, you could simply just take control over the GUILD_EMOJIS_UPDATE event and prevent anyone from caching any @@ -362,14 +362,10 @@ Someone once asked if it was possible to make Discordeno, show the number of users currently typing in the server. He had managed to build this himself in his bot, but he wanted to do it inside the library itself. In order to keep Discordeno minimalistic and memory efficient I avoided adding this. So let's see -how we could achieve this same thing with Controllers. +how we could achieve this same thing: ```ts -import { - controllers, - eventHandlers, - TypingStartPayload, -} from "../../../deps.ts"; +import { eventHandlers, handlers, TypingStartPayload } from "../../../deps.ts"; const typingUsers = new Map(); @@ -379,9 +375,7 @@ function createTimeout(userID: String) { }, 10000); } -controllers.TYPING_START = function (data) { - if (data.t !== "TYPING_START") return; - +handlers.TYPING_START = function (data) { const payload = data.d as TypingStartPayload; eventHandlers.typingStart?.(payload); @@ -393,18 +387,16 @@ controllers.TYPING_START = function (data) { }; ``` -Controllers are amazing in so many ways. This is just a basic example but it's -true potential is only limited by your imagination. I would love to see what you -all can create with controllers. +This is just a basic example but it's true potential is only limited by your +imagination. I would love to see what you all can create. -Something worth noting about why Discordeno controllers are so amazing is that -it allows you to never depend on me. When Discord releases something new, you -don't need to wait for me to update the library to access it. Without -controllers, if you wanted access to a feature you would need to wait for the -library to be updated or have to fork it, modify it and modify your code for it. -Then when the library does get updated, you need to switch back to it and modify -your code again possibly to how the lib designed it. With controllers, you never -have to fork or anything. Just take control! +Something worth noting about why Discordeno handlers are so amazing is that it +allows you to never depend on me. When Discord releases something new, you don't +need to wait for me to update the library to access it. Without handlers, if you +wanted access to a feature you would need to wait for the library to be updated +or have to fork it, modify it and modify your code for it. Then when the library +does get updated, you need to switch back to it and modify your code again +possibly to how the lib designed it. With handlers, you never have to fork or +anything. Just take control! -Controllers are extremely powerful. **Remember with great power comes great -bugs!** +Remember with great power comes great bugs! diff --git a/mod.ts b/mod.ts index d7d9a2bd5..35de54e84 100644 --- a/mod.ts +++ b/mod.ts @@ -1,27 +1,14 @@ -export * from "./src/api/controllers/bans.ts"; -export * from "./src/api/controllers/cache.ts"; -export * from "./src/api/controllers/channels.ts"; -export * from "./src/api/controllers/guilds.ts"; -export * from "./src/api/controllers/members.ts"; -export * from "./src/api/controllers/messages.ts"; -export * from "./src/api/controllers/misc.ts"; -export * from "./src/api/controllers/mod.ts"; -export * from "./src/api/controllers/reactions.ts"; -export * from "./src/api/controllers/roles.ts"; -export * from "./src/api/handlers/channel.ts"; -export * from "./src/api/handlers/guild.ts"; -export * from "./src/api/handlers/member.ts"; -export * from "./src/api/handlers/message.ts"; -export * from "./src/api/handlers/oauth.ts"; -export * from "./src/api/handlers/webhook.ts"; -export * from "./src/api/structures/channel.ts"; -export * from "./src/api/structures/guild.ts"; -export * from "./src/api/structures/member.ts"; -export * from "./src/api/structures/message.ts"; -export * from "./src/api/structures/mod.ts"; -export * from "./src/api/structures/role.ts"; export * from "./src/bot.ts"; +export * from "./src/cache.ts"; +export * from "./src/handlers/mod.ts"; +export * from "./src/helpers/mod.ts"; export * from "./src/rest/mod.ts"; +export * from "./src/structures/channel.ts"; +export * from "./src/structures/guild.ts"; +export * from "./src/structures/member.ts"; +export * from "./src/structures/message.ts"; +export * from "./src/structures/mod.ts"; +export * from "./src/structures/role.ts"; export * from "./src/types/mod.ts"; export * from "./src/util/cache.ts"; export * from "./src/util/collection.ts"; diff --git a/src/api/controllers/bans.ts b/src/api/controllers/bans.ts deleted file mode 100644 index 5e6a323b6..000000000 --- a/src/api/controllers/bans.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { DiscordPayload, GuildBanPayload } from "../../types/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalGuildBanAdd(data: DiscordPayload) { - if (data.t !== "GUILD_BAN_ADD") return; - - const payload = data.d as GuildBanPayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const member = await cacheHandlers.get("members", payload.user.id); - eventHandlers.guildBanAdd?.(guild, payload.user, member); -} - -export async function handleInternalGuildBanRemove(data: DiscordPayload) { - if (data.t !== "GUILD_BAN_REMOVE") return; - - const payload = data.d as GuildBanPayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const member = await cacheHandlers.get("members", payload.user.id); - eventHandlers.guildBanRemove?.(guild, payload.user, member); -} diff --git a/src/api/controllers/guilds.ts b/src/api/controllers/guilds.ts deleted file mode 100644 index 0f428f597..000000000 --- a/src/api/controllers/guilds.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - CreateGuildPayload, - DiscordPayload, - GuildDeletePayload, - GuildEmojisUpdatePayload, - GuildUpdateChange, - UpdateGuildPayload, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalGuildCreate( - data: DiscordPayload, - shardID: number, -) { - if (data.t !== "GUILD_CREATE") return; - - const payload = data.d as CreateGuildPayload; - // When shards resume they emit GUILD_CREATE again. - if (await cacheHandlers.has("guilds", payload.id)) return; - - const guildStruct = await structures.createGuild( - data.d as CreateGuildPayload, - shardID, - ); - - await cacheHandlers.set("guilds", guildStruct.id, guildStruct); - - if (await cacheHandlers.has("unavailableGuilds", payload.id)) { - await cacheHandlers.delete("unavailableGuilds", payload.id); - } - - if (!cache.isReady) return eventHandlers.guildLoaded?.(guildStruct); - eventHandlers.guildCreate?.(guildStruct); -} - -export async function handleInternalGuildDelete(data: DiscordPayload) { - if (data.t !== "GUILD_DELETE") return; - - const payload = data.d as GuildDeletePayload; - cacheHandlers.forEach("messages", (message) => { - if (message.guildID === payload.id) { - cacheHandlers.delete("messages", message.id); - } - }); - - cacheHandlers.forEach("channels", (channel) => { - if (channel.guildID === payload.id) { - cacheHandlers.delete("channels", channel.id); - } - }); - - if (payload.unavailable) { - return cacheHandlers.set("unavailableGuilds", payload.id, Date.now()); - } - - const guild = await cacheHandlers.get("guilds", payload.id); - if (!guild) return; - - await cacheHandlers.delete("guilds", payload.id); - - eventHandlers.guildDelete?.(guild); -} - -export async function handleInternalGuildUpdate(data: DiscordPayload) { - if (data.t !== "GUILD_UPDATE") return; - - const payload = data.d as UpdateGuildPayload; - const cachedGuild = await cacheHandlers.get("guilds", payload.id); - if (!cachedGuild) return; - - const keysToSkip = [ - "roles", - "guild_hashes", - "guild_id", - "max_members", - "emojis", - ]; - - const changes = Object.entries(payload) - .map(([key, value]) => { - if (keysToSkip.includes(key)) return; - - // @ts-ignore index signature - const cachedValue = cachedGuild[key]; - if (cachedValue !== value) { - // Guild create sends undefined and update sends false. - if (!cachedValue && !value) return; - - if (Array.isArray(cachedValue) && Array.isArray(value)) { - const different = (cachedValue.length !== value.length) || - cachedValue.find((val) => !value.includes(val)) || - value.find((val) => !cachedValue.includes(val)); - if (!different) return; - } - - // @ts-ignore index signature - cachedGuild[key] = value; - return { key, oldValue: cachedValue, value }; - } - }).filter((change) => change) as GuildUpdateChange[]; - - await cacheHandlers.set("guilds", payload.id, cachedGuild); - - eventHandlers.guildUpdate?.(cachedGuild, changes); -} - -export async function handleInternalGuildEmojisUpdate(data: DiscordPayload) { - if (data.t !== "GUILD_EMOJIS_UPDATE") return; - - const payload = data.d as GuildEmojisUpdatePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const cachedEmojis = guild.emojis; - guild.emojis = payload.emojis; - - cacheHandlers.set("guilds", payload.guild_id, guild); - - eventHandlers.guildEmojisUpdate?.( - guild, - payload.emojis, - cachedEmojis, - ); -} diff --git a/src/api/controllers/interactions.ts b/src/api/controllers/interactions.ts deleted file mode 100644 index b6feca382..000000000 --- a/src/api/controllers/interactions.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - ApplicationCommandEvent, - DiscordPayload, - InteractionCommandPayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalInteractionCreate(data: DiscordPayload) { - if (data.t !== "INTERACTION_CREATE") return; - - const payload = data.d as InteractionCommandPayload; - const memberStruct = await structures.createMember( - payload.member, - payload.guild_id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - eventHandlers.interactionCreate?.( - { - ...payload, - member: memberStruct, - }, - ); -} - -export function handleInternalApplicationCommandCreate( - data: DiscordPayload, -) { - if (data.t !== "APPLICATION_COMMAND_CREATE") return; - - const { - guild_id: guildID, - application_id: applicationID, - ...rest - } = data.d as ApplicationCommandEvent; - - eventHandlers.applicationCommandCreate?.({ - ...rest, - guildID, - applicationID, - }); -} - -export function handleInternalApplicationCommandUpdate(data: DiscordPayload) { - if (data.t !== "APPLICATION_COMMAND_UPDATE") return; - - const { - application_id: applicationID, - guild_id: guildID, - ...rest - } = data.d as ApplicationCommandEvent; - - eventHandlers.applicationCommandUpdate?.({ - ...rest, - guildID, - applicationID, - }); -} - -export function handleInternalApplicationCommandDelete(data: DiscordPayload) { - if (data.t !== "APPLICATION_COMMAND_DELETE") return; - - const { - application_id: applicationID, - guild_id: guildID, - ...rest - } = data.d as ApplicationCommandEvent; - - eventHandlers.applicationCommandDelete?.({ - ...rest, - guildID, - applicationID, - }); -} diff --git a/src/api/controllers/members.ts b/src/api/controllers/members.ts deleted file mode 100644 index 9c2c83aca..000000000 --- a/src/api/controllers/members.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - DiscordPayload, - GuildBanPayload, - GuildMemberAddPayload, - GuildMemberChunkPayload, - GuildMemberUpdatePayload, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { Collection } from "../../util/collection.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalGuildMemberAdd(data: DiscordPayload) { - if (data.t !== "GUILD_MEMBER_ADD") return; - - const payload = data.d as GuildMemberAddPayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - guild.memberCount++; - const memberStruct = await structures.createMember( - payload, - payload.guild_id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - eventHandlers.guildMemberAdd?.(guild, memberStruct); -} - -export async function handleInternalGuildMemberRemove(data: DiscordPayload) { - if (data.t !== "GUILD_MEMBER_REMOVE") return; - - const payload = data.d as GuildBanPayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - guild.memberCount--; - const member = await cacheHandlers.get("members", payload.user.id); - eventHandlers.guildMemberRemove?.(guild, payload.user, member); - - member?.guilds.delete(guild.id); - if (member && !member.guilds.size) { - await cacheHandlers.delete("members", member.id); - } -} - -export async function handleInternalGuildMemberUpdate(data: DiscordPayload) { - if (data.t !== "GUILD_MEMBER_UPDATE") return; - - const payload = data.d as GuildMemberUpdatePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const cachedMember = await cacheHandlers.get("members", payload.user.id); - const guildMember = cachedMember?.guilds.get(payload.guild_id); - - const newMemberData = { - ...payload, - // deno-lint-ignore camelcase - premium_since: payload.premium_since || undefined, - // deno-lint-ignore camelcase - joined_at: new Date(guildMember?.joinedAt || Date.now()) - .toISOString(), - deaf: guildMember?.deaf || false, - mute: guildMember?.mute || false, - roles: payload.roles, - }; - const memberStruct = await structures.createMember( - newMemberData, - payload.guild_id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - if (guildMember?.nick !== payload.nick) { - eventHandlers.nicknameUpdate?.( - guild, - memberStruct, - payload.nick, - guildMember?.nick, - ); - } - - if (payload.pending === false && guildMember?.pending === true) { - eventHandlers.membershipScreeningPassed?.(guild, memberStruct); - } - - const roleIDs = guildMember?.roles || []; - - roleIDs.forEach((id) => { - if (!payload.roles.includes(id)) { - eventHandlers.roleLost?.(guild, memberStruct, id); - } - }); - - payload.roles.forEach((id) => { - if (!roleIDs.includes(id)) { - eventHandlers.roleGained?.(guild, memberStruct, id); - } - }); - - eventHandlers.guildMemberUpdate?.(guild, memberStruct, cachedMember); -} - -export async function handleInternalGuildMembersChunk(data: DiscordPayload) { - if (data.t !== "GUILD_MEMBERS_CHUNK") return; - - const payload = data.d as GuildMemberChunkPayload; - - const members = await Promise.all( - payload.members.map(async (member) => { - const memberStruct = await structures.createMember( - member, - payload.guild_id, - ); - - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - return memberStruct; - }), - ); - - // Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming - if ( - payload.nonce - ) { - const resolve = cache.fetchAllMembersProcessingRequests.get(payload.nonce); - if (!resolve) return; - - if (payload.chunk_index + 1 === payload.chunk_count) { - cache.fetchAllMembersProcessingRequests.delete(payload.nonce); - // Only 1 chunk most likely is all members or users only request a small amount of users - if (payload.chunk_count === 1) { - return resolve(new Collection(members.map((m) => [m.id, m]))); - } - - return resolve( - await cacheHandlers.filter( - "members", - (m) => m.guilds.has(payload.guild_id), - ), - ); - } - } -} diff --git a/src/api/controllers/messages.ts b/src/api/controllers/messages.ts deleted file mode 100644 index cc4a319a4..000000000 --- a/src/api/controllers/messages.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - DiscordPayload, - MessageCreateOptions, - MessageDeleteBulkPayload, - MessageDeletePayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalMessageCreate(data: DiscordPayload) { - if (data.t !== "MESSAGE_CREATE") return; - - const payload = data.d as MessageCreateOptions; - const channel = await cacheHandlers.get("channels", payload.channel_id); - if (channel) channel.lastMessageID = payload.id; - - const guild = payload.guild_id - ? await cacheHandlers.get("guilds", payload.guild_id) - : undefined; - - if (payload.member && guild) { - // If in a guild cache the author as a member - const memberStruct = await structures.createMember( - { ...payload.member, user: payload.author }, - guild.id, - ); - - await cacheHandlers.set("members", memberStruct.id, memberStruct); - } - - await Promise.all(payload.mentions.map(async (mention) => { - // Cache the member if its a valid member - if (mention.member && guild) { - const memberStruct = await structures.createMember( - { ...mention.member, user: mention }, - guild.id, - ); - - return cacheHandlers.set("members", memberStruct.id, memberStruct); - } - })); - - const message = await structures.createMessage(payload); - // Cache the message - await cacheHandlers.set("messages", payload.id, message); - - eventHandlers.messageCreate?.(message); -} - -export async function handleInternalMessageDelete(data: DiscordPayload) { - if (data.t !== "MESSAGE_DELETE") return; - - const payload = data.d as MessageDeletePayload; - const channel = await cacheHandlers.get("channels", payload.channel_id); - if (!channel) return; - - eventHandlers.messageDelete?.( - { id: payload.id, channel }, - await cacheHandlers.get("messages", payload.id), - ); - - await cacheHandlers.delete("messages", payload.id); -} - -export async function handleInternalMessageDeleteBulk(data: DiscordPayload) { - if (data.t !== "MESSAGE_DELETE_BULK") return; - - const payload = data.d as MessageDeleteBulkPayload; - const channel = await cacheHandlers.get("channels", payload.channel_id); - if (!channel) return; - - return Promise.all(payload.ids.map(async (id) => { - eventHandlers.messageDelete?.( - { id, channel }, - await cacheHandlers.get("messages", id), - ); - await cacheHandlers.delete("messages", id); - })); -} - -export async function handleInternalMessageUpdate(data: DiscordPayload) { - if (data.t !== "MESSAGE_UPDATE") return; - - const payload = data.d as MessageCreateOptions; - const channel = await cacheHandlers.get("channels", payload.channel_id); - if (!channel) return; - - const cachedMessage = await cacheHandlers.get("messages", payload.id); - if (!cachedMessage) return; - - const oldMessage = { - attachments: cachedMessage.attachments, - content: cachedMessage.content, - embeds: cachedMessage.embeds, - editedTimestamp: cachedMessage.editedTimestamp, - tts: cachedMessage.tts, - pinned: cachedMessage.pinned, - }; - - // Messages with embeds can trigger update but they wont have edited_timestamp - if ( - !payload.edited_timestamp || - (cachedMessage.content === payload.content) - ) { - return; - } - - const message = await structures.createMessage(payload); - - await cacheHandlers.set("messages", payload.id, message); - - eventHandlers.messageUpdate?.(message, oldMessage); -} diff --git a/src/api/controllers/misc.ts b/src/api/controllers/misc.ts deleted file mode 100644 index 3a4a87dd6..000000000 --- a/src/api/controllers/misc.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { eventHandlers, setApplicationID, setBotID } from "../../bot.ts"; -import { - DiscordPayload, - IntegrationCreateUpdateEvent, - IntegrationDeleteEvent, - InviteCreateEvent, - InviteDeleteEvent, - PresenceUpdatePayload, - ReadyPayload, - TypingStartPayload, - UserPayload, - VoiceStateUpdatePayload, - WebhookUpdatePayload, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { delay } from "../../util/utils.ts"; -import { allowNextShard } from "../../ws/shard_manager.ts"; -import { initialMemberLoadQueue } from "../structures/guild.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -/** This function is the internal handler for the ready event. Users can override this with controllers if desired. */ -export async function handleInternalReady( - data: DiscordPayload, - shardID: number, -) { - if (data.t !== "READY") return; - - const payload = data.d as ReadyPayload; - setBotID(payload.user.id); - setApplicationID(payload.application.id); - - // Triggered on each shard - eventHandlers.shardReady?.(shardID); - if (payload.shard && shardID === payload.shard[1] - 1) { - const loadedAllGuilds = async () => { - const guildsMissing = async () => { - for (const g of payload.guilds) { - if (!(await cacheHandlers.has("guilds", g.id))) return true; - } - return false; - }; - - if (await guildsMissing()) { - setTimeout(loadedAllGuilds, 2000); - } else { - // The bot has already started, the last shard is resumed, however. - if (cache.isReady) return; - - cache.isReady = true; - eventHandlers.ready?.(); - - // All the members that came in on guild creates should now be processed 1 by 1 - for (const [guildID, members] of initialMemberLoadQueue.entries()) { - await Promise.all( - members.map(async (member) => { - const memberStruct = await structures.createMember( - member, - guildID, - ); - - return cacheHandlers.set( - "members", - memberStruct.id, - memberStruct, - ); - }), - ); - } - } - }; - - setTimeout(loadedAllGuilds, 2000); - } - - // Wait 5 seconds to spawn next shard - await delay(5000); - allowNextShard(); -} - -/** This function is the internal handler for the presence update event. Users can override this with controllers if desired. */ -export async function handleInternalPresenceUpdate(data: DiscordPayload) { - if (data.t !== "PRESENCE_UPDATE") return; - - const payload = data.d as PresenceUpdatePayload; - const oldPresence = await cacheHandlers.get("presences", payload.user.id); - await cacheHandlers.set("presences", payload.user.id, payload); - - eventHandlers.presenceUpdate?.(payload, oldPresence); -} - -/** This function is the internal handler for the typings event. Users can override this with controllers if desired. */ -export function handleInternalTypingStart(data: DiscordPayload) { - if (data.t !== "TYPING_START") return; - eventHandlers.typingStart?.(data.d as TypingStartPayload); -} - -/** This function is the internal handler for the user update event. Users can override this with controllers if desired. */ -export async function handleInternalUserUpdate(data: DiscordPayload) { - if (data.t !== "USER_UPDATE") return; - - const userData = data.d as UserPayload; - - const member = await cacheHandlers.get("members", userData.id); - if (!member) return; - - Object.entries(userData).forEach(([key, value]) => { - // @ts-ignore index signatures - if (member[key] !== value) return member[key] = value; - }); - - await cacheHandlers.set("members", userData.id, member); - - eventHandlers.botUpdate?.(userData); -} - -/** This function is the internal handler for the voice state update event. Users can override this with controllers if desired. */ -export async function handleInternalVoiceStateUpdate(data: DiscordPayload) { - if (data.t !== "VOICE_STATE_UPDATE") return; - - const payload = data.d as VoiceStateUpdatePayload; - if (!payload.guild_id) return; - - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const member = payload.member - ? await structures.createMember(payload.member, guild.id) - : await cacheHandlers.get("members", payload.user_id); - if (!member) return; - - // No cached state before so lets make one for em - const cachedState = guild.voiceStates.get(payload.user_id); - - guild.voiceStates.set(payload.user_id, { - ...payload, - guildID: payload.guild_id, - channelID: payload.channel_id || "", - userID: payload.user_id, - sessionID: payload.session_id, - selfDeaf: payload.self_deaf, - selfMute: payload.self_mute, - selfStream: payload.self_stream || false, - }); - - await cacheHandlers.set("guilds", payload.guild_id, guild); - - if (cachedState?.channelID !== payload.channel_id) { - // Either joined or moved channels - if (payload.channel_id) { - if (cachedState?.channelID) { // Was in a channel before - eventHandlers.voiceChannelSwitch?.( - member, - payload.channel_id, - cachedState.channelID, - ); - } else { // Was not in a channel before so user just joined - eventHandlers.voiceChannelJoin?.(member, payload.channel_id); - } - } // Left the channel - else if (cachedState?.channelID) { - guild.voiceStates.delete(payload.user_id); - eventHandlers.voiceChannelLeave?.(member, cachedState.channelID); - } - } - - eventHandlers.voiceStateUpdate?.(member, payload); -} - -/** This function is the internal handler for the webhooks update event. Users can override this with controllers if desired. */ -export function handleInternalWebhooksUpdate(data: DiscordPayload) { - if (data.t !== "WEBHOOKS_UPDATE") return; - - const options = data.d as WebhookUpdatePayload; - eventHandlers.webhooksUpdate?.( - options.channel_id, - options.guild_id, - ); -} - -export function handleInternalIntegrationCreate( - data: DiscordPayload, -) { - if (data.t !== "INTEGRATION_CREATE") return; - - const { - guild_id: guildID, - enable_emoticons: enableEmoticons, - expire_behavior: expireBehavior, - expire_grace_period: expireGracePeriod, - subscriber_count: subscriberCount, - role_id: roleID, - synced_at: syncedAt, - ...rest - } = data.d as IntegrationCreateUpdateEvent; - - eventHandlers.integrationCreate?.({ - ...rest, - guildID, - enableEmoticons, - expireBehavior, - expireGracePeriod, - syncedAt, - subscriberCount, - roleID, - }); -} - -export function handleInternalIntegrationUpdate(data: DiscordPayload) { - if (data.t !== "INTEGRATION_UPDATE") return; - - const { - enable_emoticons: enableEmoticons, - expire_behavior: expireBehavior, - expire_grace_period: expireGracePeriod, - role_id: roleID, - subscriber_count: subscriberCount, - synced_at: syncedAt, - guild_id: guildID, - ...rest - } = data.d as IntegrationCreateUpdateEvent; - - eventHandlers.integrationUpdate?.({ - ...rest, - guildID, - subscriberCount, - enableEmoticons, - expireGracePeriod, - roleID, - expireBehavior, - syncedAt, - }); -} - -export function handleInternalIntegrationDelete(data: DiscordPayload) { - if (data.t !== "INTEGRATION_DELETE") return; - - const { - guild_id: guildID, - application_id: applicationID, - ...rest - } = data.d as IntegrationDeleteEvent; - - eventHandlers.integrationDelete?.({ - ...rest, - applicationID, - guildID, - }); -} - -export function handleInternalInviteCreate(payload: DiscordPayload) { - if (payload.t !== "INVITE_CREATE") return; - - const { - channel_id: channelID, - created_at: createdAt, - max_age: maxAge, - guild_id: guildID, - target_user: targetUser, - target_user_type: targetUserType, - max_uses: maxUses, - ...rest - } = payload.d as InviteCreateEvent; - - eventHandlers.inviteCreate?.({ - ...rest, - channelID, - guildID, - maxAge, - targetUser, - targetUserType, - maxUses, - createdAt, - }); -} - -export function handleInternalInviteDelete(payload: DiscordPayload) { - if (payload.t !== "INVITE_DELETE") return; - - const { - channel_id: channelID, - guild_id: guildID, - ...rest - } = payload.d as InviteDeleteEvent; - - eventHandlers.inviteDelete?.({ - ...rest, - channelID, - guildID, - }); -} diff --git a/src/api/controllers/mod.ts b/src/api/controllers/mod.ts deleted file mode 100644 index ce97927b5..000000000 --- a/src/api/controllers/mod.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - handleInternalGuildBanAdd, - handleInternalGuildBanRemove, -} from "./bans.ts"; -import { - handleInternalChannelCreate, - handleInternalChannelDelete, - handleInternalChannelUpdate, -} from "./channels.ts"; -import { - handleInternalGuildCreate, - handleInternalGuildDelete, - handleInternalGuildEmojisUpdate, - handleInternalGuildUpdate, -} from "./guilds.ts"; -import { - handleInternalApplicationCommandCreate, - handleInternalApplicationCommandDelete, - handleInternalApplicationCommandUpdate, - handleInternalInteractionCreate, -} from "./interactions.ts"; -import { - handleInternalGuildMemberAdd, - handleInternalGuildMemberRemove, - handleInternalGuildMembersChunk, - handleInternalGuildMemberUpdate, -} from "./members.ts"; -import { - handleInternalMessageCreate, - handleInternalMessageDelete, - handleInternalMessageDeleteBulk, - handleInternalMessageUpdate, -} from "./messages.ts"; -import { - handleInternalIntegrationCreate, - handleInternalIntegrationDelete, - handleInternalIntegrationUpdate, - handleInternalInviteCreate, - handleInternalInviteDelete, - handleInternalPresenceUpdate, - handleInternalReady, - handleInternalTypingStart, - handleInternalUserUpdate, - handleInternalVoiceStateUpdate, - handleInternalWebhooksUpdate, -} from "./misc.ts"; -import { - handleInternalMessageReactionAdd, - handleInternalMessageReactionRemove, - handleInternalMessageReactionRemoveAll, - handleInternalMessageReactionRemoveEmoji, -} from "./reactions.ts"; -import { - handleInternalGuildRoleCreate, - handleInternalGuildRoleDelete, - handleInternalGuildRoleUpdate, -} from "./roles.ts"; - -export let controllers = { - READY: handleInternalReady, - CHANNEL_CREATE: handleInternalChannelCreate, - CHANNEL_DELETE: handleInternalChannelDelete, - CHANNEL_UPDATE: handleInternalChannelUpdate, - GUILD_CREATE: handleInternalGuildCreate, - GUILD_DELETE: handleInternalGuildDelete, - GUILD_UPDATE: handleInternalGuildUpdate, - GUILD_BAN_ADD: handleInternalGuildBanAdd, - GUILD_BAN_REMOVE: handleInternalGuildBanRemove, - GUILD_EMOJIS_UPDATE: handleInternalGuildEmojisUpdate, - GUILD_MEMBER_ADD: handleInternalGuildMemberAdd, - GUILD_MEMBER_REMOVE: handleInternalGuildMemberRemove, - GUILD_MEMBER_UPDATE: handleInternalGuildMemberUpdate, - GUILD_MEMBERS_CHUNK: handleInternalGuildMembersChunk, - GUILD_ROLE_CREATE: handleInternalGuildRoleCreate, - GUILD_ROLE_DELETE: handleInternalGuildRoleDelete, - GUILD_ROLE_UPDATE: handleInternalGuildRoleUpdate, - INTERACTION_CREATE: handleInternalInteractionCreate, - APPLICATION_COMMAND_CREATE: handleInternalApplicationCommandCreate, - APPLICATION_COMMAND_DELETE: handleInternalApplicationCommandDelete, - APPLICATION_COMMAND_UPDATE: handleInternalApplicationCommandUpdate, - MESSAGE_CREATE: handleInternalMessageCreate, - MESSAGE_DELETE: handleInternalMessageDelete, - MESSAGE_DELETE_BULK: handleInternalMessageDeleteBulk, - MESSAGE_UPDATE: handleInternalMessageUpdate, - MESSAGE_REACTION_ADD: handleInternalMessageReactionAdd, - MESSAGE_REACTION_REMOVE: handleInternalMessageReactionRemove, - MESSAGE_REACTION_REMOVE_ALL: handleInternalMessageReactionRemoveAll, - MESSAGE_REACTION_REMOVE_EMOJI: handleInternalMessageReactionRemoveEmoji, - PRESENCE_UPDATE: handleInternalPresenceUpdate, - TYPING_START: handleInternalTypingStart, - USER_UPDATE: handleInternalUserUpdate, - VOICE_STATE_UPDATE: handleInternalVoiceStateUpdate, - WEBHOOKS_UPDATE: handleInternalWebhooksUpdate, - INTEGRATION_CREATE: handleInternalIntegrationCreate, - INTEGRATION_UPDATE: handleInternalIntegrationUpdate, - INTEGRATION_DELETE: handleInternalIntegrationDelete, - INVITE_CREATE: handleInternalInviteCreate, - INVITE_DELETE: handleInternalInviteDelete, -}; - -export type Controllers = typeof controllers; - -export function updateControllers(newControllers: Controllers) { - controllers = { - ...controllers, - ...newControllers, - }; -} diff --git a/src/api/controllers/reactions.ts b/src/api/controllers/reactions.ts deleted file mode 100644 index 50e83d0bb..000000000 --- a/src/api/controllers/reactions.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { botID, eventHandlers } from "../../bot.ts"; -import { - BaseMessageReactionPayload, - DiscordPayload, - MessageReactionPayload, - MessageReactionRemoveEmojiPayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalMessageReactionAdd(data: DiscordPayload) { - if (data.t !== "MESSAGE_REACTION_ADD") return; - - const payload = data.d as MessageReactionPayload; - const message = await cacheHandlers.get("messages", payload.message_id); - - if (message) { - const reactionExisted = message.reactions?.find( - (reaction) => - reaction.emoji.id === payload.emoji.id && - reaction.emoji.name === payload.emoji.name, - ); - - if (reactionExisted) reactionExisted.count++; - else { - const newReaction = { - count: 1, - me: payload.user_id === botID, - emoji: { ...payload.emoji, id: payload.emoji.id || undefined }, - }; - message.reactions = message.reactions - ? [...message.reactions, newReaction] - : [newReaction]; - } - - await cacheHandlers.set("messages", payload.message_id, message); - } - - if (payload.member && payload.guild_id) { - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (guild) { - const memberStruct = await structures.createMember( - payload.member, - guild.id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - } - } - - const uncachedOptions = { - ...payload, - id: payload.message_id, - channelID: payload.channel_id, - guildID: payload.guild_id || "", - }; - - eventHandlers.reactionAdd?.( - uncachedOptions, - payload.emoji, - payload.user_id, - message, - ); -} - -export async function handleInternalMessageReactionRemove( - data: DiscordPayload, -) { - if (data.t !== "MESSAGE_REACTION_REMOVE") return; - - const payload = data.d as MessageReactionPayload; - const message = await cacheHandlers.get("messages", payload.message_id); - - if (message) { - const reactionExisted = message.reactions?.find( - (reaction) => - reaction.emoji.id === payload.emoji.id && - reaction.emoji.name === payload.emoji.name, - ); - - if (reactionExisted) reactionExisted.count--; - else { - const newReaction = { - count: 1, - me: payload.user_id === botID, - emoji: { ...payload.emoji, id: payload.emoji.id || undefined }, - }; - message.reactions = message.reactions - ? [...message.reactions, newReaction] - : [newReaction]; - } - - await cacheHandlers.set("messages", payload.message_id, message); - } - - if (payload.member && payload.guild_id) { - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (guild) { - const memberStruct = await structures.createMember( - payload.member, - guild.id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - } - } - - const uncachedOptions = { - ...payload, - id: payload.message_id, - channelID: payload.channel_id, - guildID: payload.guild_id, - }; - - eventHandlers.reactionRemove?.( - uncachedOptions, - payload.emoji, - payload.user_id, - message, - ); -} - -export async function handleInternalMessageReactionRemoveAll( - data: DiscordPayload, -) { - if (data.t !== "MESSAGE_REACTION_REMOVE_ALL") return; - - const payload = data.d as BaseMessageReactionPayload; - const message = await cacheHandlers.get("messages", payload.message_id); - - if (message?.reactions) { - message.reactions = undefined; - - await cacheHandlers.set("messages", payload.message_id, message); - } - - eventHandlers.reactionRemoveAll?.(data.d as BaseMessageReactionPayload); -} - -export async function handleInternalMessageReactionRemoveEmoji( - data: DiscordPayload, -) { - if (data.t !== "MESSAGE_REACTION_REMOVE_EMOJI") return; - - const payload = data.d as MessageReactionRemoveEmojiPayload; - const message = await cacheHandlers.get("messages", payload.message_id); - - if (message?.reactions) { - message.reactions = message.reactions?.filter( - (reaction) => - !( - reaction.emoji.id === payload.emoji.id && - reaction.emoji.name === payload.emoji.name - ), - ); - - await cacheHandlers.set("messages", payload.message_id, message); - } - - eventHandlers.reactionRemoveEmoji?.( - data.d as MessageReactionRemoveEmojiPayload, - ); -} diff --git a/src/api/controllers/roles.ts b/src/api/controllers/roles.ts deleted file mode 100644 index 63292058e..000000000 --- a/src/api/controllers/roles.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - DiscordPayload, - GuildRoleDeletePayload, - GuildRolePayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalGuildRoleCreate(data: DiscordPayload) { - if (data.t !== "GUILD_ROLE_CREATE") return; - - const payload = data.d as GuildRolePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const role = await structures.createRole(payload.role); - guild.roles = guild.roles.set(payload.role.id, role); - await cacheHandlers.set("guilds", payload.guild_id, guild); - - eventHandlers.roleCreate?.(guild, role); -} - -export async function handleInternalGuildRoleDelete(data: DiscordPayload) { - if (data.t !== "GUILD_ROLE_DELETE") return; - - const payload = data.d as GuildRoleDeletePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const cachedRole = guild.roles.get(payload.role_id)!; - guild.roles.delete(payload.role_id); - - if (cachedRole) eventHandlers.roleDelete?.(guild, cachedRole); - - // For bots without GUILD_MEMBERS member.roles is never updated breaking permissions checking. - cacheHandlers.forEach("members", (member) => { - // Not in the relevant guild so just skip. - if (!member.guilds.has(guild.id)) return; - - member.guilds.forEach((g) => { - // Member does not have this role - if (!g.roles.includes(payload.role_id)) return; - // Remove this role from the members cache - g.roles = g.roles.filter((id) => id !== payload.role_id); - cacheHandlers.set("members", member.id, member); - }); - }); -} - -export async function handleInternalGuildRoleUpdate(data: DiscordPayload) { - if (data.t !== "GUILD_ROLE_UPDATE") return; - - const payload = data.d as GuildRolePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const cachedRole = guild.roles.get(payload.role.id); - if (!cachedRole) return; - - const role = await structures.createRole(payload.role); - guild.roles.set(payload.role.id, role); - await cacheHandlers.set("guilds", guild.id, guild); - - eventHandlers.roleUpdate?.(guild, role, cachedRole); -} diff --git a/src/api/handlers/channel.ts b/src/api/handlers/channel.ts deleted file mode 100644 index 5e1b772d1..000000000 --- a/src/api/handlers/channel.ts +++ /dev/null @@ -1,565 +0,0 @@ -import { RequestManager } from "../../rest/request_manager.ts"; -import { - ChannelEditOptions, - ChannelTypes, - CreateInviteOptions, - Errors, - FollowedChannelPayload, - GetMessages, - GetMessagesAfter, - GetMessagesAround, - GetMessagesBefore, - InvitePayload, - MessageContent, - MessageCreateOptions, - Permission, - Permissions, - RawOverwrite, - WebhookPayload, -} from "../../types/mod.ts"; -import { endpoints } from "../../util/constants.ts"; -import { - botHasChannelPermissions, - botHasPermission, - calculateBits, -} from "../../util/permissions.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; -import { structures } from "../structures/mod.ts"; - -/** Checks if a channel overwrite for a user id or a role id has permission in this channel */ -export function channelOverwriteHasPermission( - guildID: string, - id: string, - overwrites: RawOverwrite[], - permissions: Permission[], -) { - const overwrite = overwrites.find((perm) => perm.id === id) || - overwrites.find((perm) => perm.id === guildID); - - return permissions.every((perm) => { - if (overwrite) { - const allowBits = overwrite.allow; - const denyBits = overwrite.deny; - if (BigInt(denyBits) & BigInt(Permissions[perm])) return false; - if (BigInt(allowBits) & BigInt(Permissions[perm])) return true; - } - return false; - }); -} - -/** Fetch a single message from the server. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */ -export async function getMessage( - channelID: string, - id: string, -) { - const hasViewChannelPerm = await botHasChannelPermissions( - channelID, - ["VIEW_CHANNEL"], - ); - if ( - !hasViewChannelPerm - ) { - throw new Error(Errors.MISSING_VIEW_CHANNEL); - } - - const hasReadMessageHistoryPerm = await botHasChannelPermissions( - channelID, - ["READ_MESSAGE_HISTORY"], - ); - if ( - !hasReadMessageHistoryPerm - ) { - throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); - } - - const result = await RequestManager.get( - endpoints.CHANNEL_MESSAGE(channelID, id), - ) as MessageCreateOptions; - - return structures.createMessage(result); -} - -/** Fetches between 2-100 messages. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */ -export async function getMessages( - channelID: string, - options?: - | GetMessagesAfter - | GetMessagesBefore - | GetMessagesAround - | GetMessages, -) { - const hasViewChannelPerm = await botHasChannelPermissions( - channelID, - ["VIEW_CHANNEL"], - ); - if ( - !hasViewChannelPerm - ) { - throw new Error(Errors.MISSING_VIEW_CHANNEL); - } - - const hasReadMessageHistoryPerm = await botHasChannelPermissions( - channelID, - ["READ_MESSAGE_HISTORY"], - ); - if ( - !hasReadMessageHistoryPerm - ) { - throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); - } - - if (options?.limit && options.limit > 100) return; - - const result = (await RequestManager.get( - endpoints.CHANNEL_MESSAGES(channelID), - options, - )) as MessageCreateOptions[]; - - return Promise.all(result.map((res) => structures.createMessage(res))); -} - -/** Get pinned messages in this channel. */ -export async function getPins(channelID: string) { - const result = (await RequestManager.get( - endpoints.CHANNEL_PINS(channelID), - )) as MessageCreateOptions[]; - - return Promise.all(result.map((res) => structures.createMessage(res))); -} - -/** - * Trigger a typing indicator for the specified channel. Generally bots should **NOT** implement this route. - * However, if a bot is responding to a command and expects the computation to take a few seconds, - * this endpoint may be called to let the user know that the bot is processing their message. - */ -export async function startTyping(channelID: string) { - const result = await RequestManager.post(endpoints.CHANNEL_TYPING(channelID)); - - return result; -} - -/** Send a message to the channel. Requires SEND_MESSAGES permission. */ -export async function sendMessage( - channelID: string, - content: string | MessageContent, -) { - if (typeof content === "string") content = { content }; - - const channel = await cacheHandlers.get("channels", channelID); - // If the channel is cached, we can do extra checks/safety - if (channel) { - if ( - ![ChannelTypes.DM, ChannelTypes.GUILD_NEWS, ChannelTypes.GUILD_TEXT] - .includes(channel.type) - ) { - throw new Error(Errors.CHANNEL_NOT_TEXT_BASED); - } - - const hasSendMessagesPerm = await botHasChannelPermissions( - channelID, - ["SEND_MESSAGES"], - ); - if ( - !hasSendMessagesPerm - ) { - throw new Error(Errors.MISSING_SEND_MESSAGES); - } - - const hasSendTtsMessagesPerm = await botHasChannelPermissions( - channelID, - ["SEND_TTS_MESSAGES"], - ); - if ( - content.tts && - !hasSendTtsMessagesPerm - ) { - throw new Error(Errors.MISSING_SEND_TTS_MESSAGE); - } - - const hasEmbedLinksPerm = await botHasChannelPermissions( - channelID, - ["EMBED_LINKS"], - ); - if ( - content.embed && - !hasEmbedLinksPerm - ) { - throw new Error(Errors.MISSING_EMBED_LINKS); - } - - if (content.mentions?.repliedUser) { - if ( - !(await botHasChannelPermissions( - channelID, - ["READ_MESSAGE_HISTORY"], - )) - ) { - throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); - } - } - } - - // Use ... for content length due to unicode characters and js .length handling - if (content.content && [...content.content].length > 2000) { - throw new Error(Errors.MESSAGE_MAX_LENGTH); - } - - if (content.mentions) { - if (content.mentions.users?.length) { - if (content.mentions.parse?.includes("users")) { - content.mentions.parse = content.mentions.parse.filter((p) => - p !== "users" - ); - } - - if (content.mentions.users.length > 100) { - content.mentions.users = content.mentions.users.slice(0, 100); - } - } - - if (content.mentions.roles?.length) { - if (content.mentions.parse?.includes("roles")) { - content.mentions.parse = content.mentions.parse.filter((p) => - p !== "roles" - ); - } - - if (content.mentions.roles.length > 100) { - content.mentions.roles = content.mentions.roles.slice(0, 100); - } - } - } - - const result = await RequestManager.post( - endpoints.CHANNEL_MESSAGES(channelID), - { - ...content, - allowed_mentions: content.mentions - ? { - ...content.mentions, - replied_user: content.mentions.repliedUser, - } - : undefined, - ...(content.replyMessageID - ? { - message_reference: { - message_id: content.replyMessageID, - }, - } - : {}), - }, - ) as MessageCreateOptions; - - return structures.createMessage(result); -} - -/** Delete messages from the channel. 2-100. Requires the MANAGE_MESSAGES permission */ -export async function deleteMessages( - channelID: string, - ids: string[], - reason?: string, -) { - const hasManageMessages = await botHasChannelPermissions( - channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessages - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } - if (ids.length < 2) { - throw new Error(Errors.DELETE_MESSAGES_MIN); - } - - if (ids.length > 100) { - console.warn( - `This endpoint only accepts a maximum of 100 messages. Deleting the first 100 message ids provided.`, - ); - } - - const result = await RequestManager.post( - endpoints.CHANNEL_BULK_DELETE(channelID), - { - messages: ids.splice(0, 100), - reason, - }, - ); - - return result; -} - -/** Gets the invites for this channel. Requires MANAGE_CHANNEL */ -export async function getChannelInvites(channelID: string) { - const hasManagaChannels = await botHasChannelPermissions( - channelID, - ["MANAGE_CHANNELS"], - ); - if ( - !hasManagaChannels - ) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } - - const result = await RequestManager.get(endpoints.CHANNEL_INVITES(channelID)); - - return result; -} - -/** Creates a new invite for this channel. Requires CREATE_INSTANT_INVITE */ -export async function createInvite( - channelID: string, - options: CreateInviteOptions, -) { - const hasCreateInstantInvitePerm = await botHasChannelPermissions( - channelID, - ["CREATE_INSTANT_INVITE"], - ); - if ( - !hasCreateInstantInvitePerm - ) { - throw new Error(Errors.MISSING_CREATE_INSTANT_INVITE); - } - - if (options.max_age && (options.max_age > 604800 || options.max_age < 0)) { - console.log( - `The max age for invite created in ${channelID} was not between 0-604800. Using default values instead.`, - ); - options.max_age = undefined; - } - - if (options.max_uses && (options.max_uses > 100 || options.max_uses < 0)) { - console.log( - `The max uses for invite created in ${channelID} was not between 0-100. Using default values instead.`, - ); - options.max_uses = undefined; - } - - const result = await RequestManager.post( - endpoints.CHANNEL_INVITES(channelID), - options, - ); - - return result; -} - -/** Returns an invite for the given code. */ -export async function getInvite(inviteCode: string) { - const result = await RequestManager.get( - endpoints.INVITE(inviteCode), - ); - - return result as InvitePayload; -} - -/** Deletes an invite for the given code. Requires `MANAGE_CHANNELS` or `MANAGE_GUILD` permission */ -export async function deleteInvite( - channelID: string, - inviteCode: string, -) { - const hasPerm = await botHasChannelPermissions(channelID, [ - "MANAGE_CHANNELS", - ]); - - if (!hasPerm) { - const channel = await cacheHandlers.get("channels", channelID); - - const hasManageGuildPerm = await botHasPermission(channel!.guildID, [ - "MANAGE_GUILD", - ]); - - if (!hasManageGuildPerm) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } - } - - const result = await RequestManager.delete( - endpoints.INVITE(inviteCode), - ); - - return result as InvitePayload; -} - -/** Gets the webhooks for this channel. Requires MANAGE_WEBHOOKS */ -export async function getChannelWebhooks(channelID: string) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } - - const result = await RequestManager.get( - endpoints.CHANNEL_WEBHOOKS(channelID), - ); - - return result as WebhookPayload[]; -} - -interface EditChannelRequest { - amount: number; - timestamp: number; - channelID: string; - items: { - channelID: string; - options: ChannelEditOptions; - }[]; -} - -const editChannelNameTopicQueue = new Map(); -let editChannelProcessing = false; - -function processEditChannelQueue() { - if (!editChannelProcessing) return; - - const now = Date.now(); - editChannelNameTopicQueue.forEach((request) => { - if (now > request.timestamp) return; - // 10 minutes have passed so we can reset this channel again - if (!request.items.length) { - return editChannelNameTopicQueue.delete(request.channelID); - } - request.amount = 0; - // There are items to process for this request - const details = request.items.shift(); - - if (!details) return; - - editChannel(details.channelID, details.options); - const secondDetails = request.items.shift(); - if (!secondDetails) return; - - return editChannel( - secondDetails.channelID, - secondDetails.options, - ); - }); - - if (editChannelNameTopicQueue.size) { - setTimeout(() => processEditChannelQueue(), 600000); - } else { - editChannelProcessing = false; - } -} - -/** Update a channel's settings. Requires the `MANAGE_CHANNELS` permission for the guild. */ -export async function editChannel( - channelID: string, - options: ChannelEditOptions, - reason?: string, -) { - const hasManageChannelsPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_CHANNELS"], - ); - if ( - !hasManageChannelsPerm - ) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } - - if (options.name || options.topic) { - const request = editChannelNameTopicQueue.get(channelID); - if (!request) { - // If this hasnt been done before simply add 1 for it - editChannelNameTopicQueue.set(channelID, { - channelID: channelID, - amount: 1, - // 10 minutes from now - timestamp: Date.now() + 600000, - items: [], - }); - } else if (request.amount === 1) { - // Start queuing future requests to this channel - request.amount = 2; - request.timestamp = Date.now() + 600000; - } else { - // 2 have already been used add to queue - request.items.push({ channelID, options }); - if (editChannelProcessing) return; - editChannelProcessing = true; - processEditChannelQueue(); - return; - } - } - - const payload = { - ...options, - // deno-lint-ignore camelcase - rate_limit_per_user: options.slowmode, - // deno-lint-ignore camelcase - parent_id: options.parentID, - // deno-lint-ignore camelcase - user_limit: options.userLimit, - // deno-lint-ignore camelcase - permission_overwrites: options.overwrites?.map( - (overwrite) => { - return { - ...overwrite, - allow: calculateBits(overwrite.allow), - deny: calculateBits(overwrite.deny), - }; - }, - ), - }; - - const result = await RequestManager.patch( - endpoints.CHANNEL_BASE(channelID), - { - ...payload, - reason, - }, - ); - - return result; -} - -/** Follow a News Channel to send messages to a target channel. Requires the `MANAGE_WEBHOOKS` permission in the target channel. Returns the webhook id. */ -export async function followChannel( - sourceChannelID: string, - targetChannelID: string, -) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - targetChannelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } - - const data = await RequestManager.post( - endpoints.CHANNEL_FOLLOW(sourceChannelID), - { - webhook_channel_id: targetChannelID, - }, - ) as FollowedChannelPayload; - - return data.webhook_id; -} - -/** - * Checks whether a channel is synchronized with its parent/category channel or not. - * @param channelID The ID of the channel to test for synchronization - * @return Returns `true` if the channel is synchronized, otherwise `false`. Returns `false` if the channel is not cached. - */ -export async function isChannelSynced(channelID: string) { - const channel = await cacheHandlers.get("channels", channelID); - if (!channel?.parentID) return false; - - const parentChannel = await cacheHandlers.get("channels", channel.parentID); - if (!parentChannel) return false; - - return channel.permissionOverwrites?.every((overwrite) => { - const permission = parentChannel.permissionOverwrites?.find((ow) => - ow.id === overwrite.id - ); - if (!permission) return false; - return !(overwrite.allow !== permission.allow || - overwrite.deny !== permission.deny); - }); -} diff --git a/src/api/handlers/guild.ts b/src/api/handlers/guild.ts deleted file mode 100644 index 9aff87160..000000000 --- a/src/api/handlers/guild.ts +++ /dev/null @@ -1,1115 +0,0 @@ -import { identifyPayload } from "../../bot.ts"; -import { RequestManager } from "../../rest/request_manager.ts"; -import { - AuditLogs, - BannedUser, - BanOptions, - ChannelCreateOptions, - ChannelCreatePayload, - ChannelTypes, - CreateEmojisOptions, - CreateGuildFromTemplate, - CreateGuildPayload, - CreateGuildTemplate, - CreateRoleOptions, - CreateServerOptions, - EditEmojisOptions, - EditGuildTemplate, - EditIntegrationOptions, - Emoji, - Errors, - FetchMembersOptions, - GetAuditLogsOptions, - GetMemberOptions, - GuildEditOptions, - GuildTemplate, - ImageFormats, - ImageSize, - Intents, - MemberCreatePayload, - Overwrite, - PositionSwap, - PruneOptions, - PrunePayload, - RoleData, - UpdateGuildPayload, - UserPayload, -} from "../../types/mod.ts"; -import { Collection } from "../../util/collection.ts"; -import { endpoints } from "../../util/constants.ts"; -import { botHasPermission, calculateBits } from "../../util/permissions.ts"; -import { formatImageURL, urlToBase64 } from "../../util/utils.ts"; -import { requestAllMembers } from "../../ws/shard_manager.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; -import { Guild, Member, structures } from "../structures/mod.ts"; - -/** Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. This endpoint can be used only by bots in less than 10 guilds. */ -export async function createServer(options: CreateServerOptions) { - const guild = await RequestManager.post( - endpoints.GUILDS, - options, - ) as CreateGuildPayload; - - return structures.createGuild(guild, 0); -} - -/** Delete a guild permanently. User must be owner. Returns 204 No Content on success. Fires a Guild Delete Gateway event. - */ -export async function deleteServer(guildID: string) { - const result = await RequestManager.delete(endpoints.GUILDS_BASE(guildID)); - - return result; -} - -/** Gets an array of all the channels ids that are the children of this category. */ -export function categoryChildrenIDs(guildID: string, id: string) { - return cacheHandlers.filter( - "channels", - (channel) => channel.parentID === id && channel.guildID === guildID, - ); -} - -/** The full URL of the icon from Discords CDN. Undefined when no icon is set. */ -export function guildIconURL( - guild: Guild, - size: ImageSize = 128, - format?: ImageFormats, -) { - return guild.icon - ? formatImageURL(endpoints.GUILD_ICON(guild.id, guild.icon), size, format) - : undefined; -} - -/** The full URL of the splash from Discords CDN. Undefined if no splash is set. */ -export function guildSplashURL( - guild: Guild, - size: ImageSize = 128, - format?: ImageFormats, -) { - return guild.splash - ? formatImageURL( - endpoints.GUILD_SPLASH(guild.id, guild.splash), - size, - format, - ) - : undefined; -} - -/** The full URL of the banner from Discords CDN. Undefined if no banner is set. */ -export function guildBannerURL( - guild: Guild, - size: ImageSize = 128, - format?: ImageFormats, -) { - return guild.banner - ? formatImageURL( - endpoints.GUILD_BANNER(guild.id, guild.banner), - size, - format, - ) - : undefined; -} - -/** Create a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */ -export async function createGuildChannel( - guild: Guild, - name: string, - options?: ChannelCreateOptions, -) { - const hasPerm = await botHasPermission( - guild.id, - ["MANAGE_CHANNELS"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } - - const result = - (await RequestManager.post(endpoints.GUILD_CHANNELS(guild.id), { - ...options, - name, - permission_overwrites: options?.permissionOverwrites?.map((perm) => ({ - ...perm, - - allow: calculateBits(perm.allow), - deny: calculateBits(perm.deny), - })), - type: options?.type || ChannelTypes.GUILD_TEXT, - })) as ChannelCreatePayload; - - const channelStruct = await structures.createChannel(result); - - await cacheHandlers.set("channels", channelStruct.id, channelStruct); - - return channelStruct; -} - -/** Delete a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */ -export async function deleteChannel( - guildID: string, - channelID: string, - reason?: string, -) { - const hasPerm = await botHasPermission( - guildID, - ["MANAGE_CHANNELS"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_CHANNELS); - } - - const guild = await cacheHandlers.get("guilds", guildID); - if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); - - if (guild?.rulesChannelID === channelID) { - throw new Error(Errors.RULES_CHANNEL_CANNOT_BE_DELETED); - } - - if (guild?.publicUpdatesChannelID === channelID) { - throw new Error(Errors.UPDATES_CHANNEL_CANNOT_BE_DELETED); - } - - const result = await RequestManager.delete( - endpoints.CHANNEL_BASE(channelID), - { reason }, - ); - - return result; -} - -/** Returns a list of guild channel objects. -* -* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your channels will be cached in your guild.** -*/ -export async function getChannels(guildID: string, addToCache = true) { - const result = await RequestManager.get( - endpoints.GUILD_CHANNELS(guildID), - ) as ChannelCreatePayload[]; - - return Promise.all(result.map(async (res) => { - const channelStruct = await structures.createChannel(res, guildID); - if (addToCache) { - await cacheHandlers.set("channels", channelStruct.id, channelStruct); - } - - return channelStruct; - })); -} - -/** Fetches a single channel object from the api. -* -* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your channels will be cached in your guild.** -*/ -export async function getChannel(channelID: string, addToCache = true) { - const result = await RequestManager.get( - endpoints.CHANNEL_BASE(channelID), - ) as ChannelCreatePayload; - - const channelStruct = await structures.createChannel(result, result.guild_id); - if (addToCache) { - await cacheHandlers.set("channels", channelStruct.id, channelStruct); - } - - return channelStruct; -} - -/** Modify the positions of channels on the guild. Requires MANAGE_CHANNELS permisison. */ -export async function swapChannels( - guildID: string, - channelPositions: PositionSwap[], -) { - if (channelPositions.length < 2) { - throw "You must provide at least two channels to be swapped."; - } - - const result = await RequestManager.patch( - endpoints.GUILD_CHANNELS(guildID), - channelPositions, - ); - - return result; -} - -/** Edit the channel permission overwrites for a user or role in this channel. Requires `MANAGE_ROLES` permission. */ -export async function editChannelOverwrite( - guildID: string, - channelID: string, - overwriteID: string, - options: Omit, -) { - const hasPerm = await botHasPermission( - guildID, - ["MANAGE_ROLES"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - const result = await RequestManager.put( - endpoints.CHANNEL_OVERWRITE(channelID, overwriteID), - { - allow: calculateBits(options.allow), - deny: calculateBits(options.deny), - type: options.type, - }, - ); - - return result; -} - -/** Delete the channel permission overwrites for a user or role in this channel. Requires `MANAGE_ROLES` permission. */ -export async function deleteChannelOverwrite( - guildID: string, - channelID: string, - overwriteID: string, -) { - const hasPerm = await botHasPermission( - guildID, - ["MANAGE_ROLES"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - const result = await RequestManager.delete( - endpoints.CHANNEL_OVERWRITE(channelID, overwriteID), - ); - - return result; -} - -/** Returns a guild member object for the specified user. -* -* ⚠️ **ADVANCED USE ONLY: Your members will be cached in your guild most likely. Only use this when you are absolutely sure the member is not cached.** -*/ -export async function getMember( - guildID: string, - id: string, - options?: { force?: boolean }, -) { - const guild = await cacheHandlers.get("guilds", guildID); - if (!guild && !options?.force) return; - - const data = await RequestManager.get( - endpoints.GUILD_MEMBER(guildID, id), - ) as MemberCreatePayload; - - const memberStruct = await structures.createMember(data, guildID); - - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - return memberStruct; -} - -/** Returns guild member objects for the specified user by their nickname/username. -* -* ⚠️ **ADVANCED USE ONLY: Your members will be cached in your guild most likely. Only use this when you are absolutely sure the member is not cached.** -*/ -export async function getMembersByQuery( - guildID: string, - name: string, - limit = 1, -) { - const guild = await cacheHandlers.get("guilds", guildID); - if (!guild) return; - - return new Promise((resolve) => { - return requestAllMembers(guild, resolve, { query: name, limit }); - }) as Promise>; -} - -/** Create an emoji in the server. Emojis and animated emojis have a maximum file size of 256kb. Attempting to upload an emoji larger than this limit will fail and return 400 Bad Request and an error message, but not a JSON status code. If a URL is provided to the image parameter, Discordeno will automatically convert it to a base64 string internally. */ -export async function createEmoji( - guildID: string, - name: string, - image: string, - options: CreateEmojisOptions, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_EMOJIS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_EMOJIS); - } - - if (image && !image.startsWith("data:image/")) { - image = await urlToBase64(image); - } - - const result = await RequestManager.post(endpoints.GUILD_EMOJIS(guildID), { - ...options, - name, - image, - }); - - return result; -} - -/** Modify the given emoji. Requires the MANAGE_EMOJIS permission. */ -export async function editEmoji( - guildID: string, - id: string, - options: EditEmojisOptions, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_EMOJIS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_EMOJIS); - } - - const result = await RequestManager.patch( - endpoints.GUILD_EMOJI(guildID, id), - { - name: options.name, - roles: options.roles, - }, - ); - - return result; -} - -/** Delete the given emoji. Requires the MANAGE_EMOJIS permission. Returns 204 No Content on success. */ -export async function deleteEmoji( - guildID: string, - id: string, - reason?: string, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_EMOJIS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_EMOJIS); - } - - const result = await RequestManager.delete( - endpoints.GUILD_EMOJI(guildID, id), - { reason }, - ); - - return result; -} - -/** Creates a url to the emoji from the Discord CDN. */ -export function emojiURL(id: string, animated = false) { - return `https://cdn.discordapp.com/emojis/${id}.${animated ? "gif" : "png"}`; -} - -/** - * Returns a list of emojis for the given guild. - * - * ⚠️ **If you need this, you are probably doing something wrong. Always use cache.guilds.get()?.emojis - */ -export async function getEmojis(guildID: string, addToCache = true) { - const result = await RequestManager.get( - endpoints.GUILD_EMOJIS(guildID), - ) as Emoji[]; - - if (addToCache) { - const guild = await cacheHandlers.get("guilds", guildID); - if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); - guild.emojis = result; - cacheHandlers.set("guilds", guildID, guild); - } - - return result; -} - -/** - * Returns an emoji for the given guild and emoji ID. - * - * ⚠️ **If you need this, you are probably doing something wrong. Always use cache.guilds.get()?.emojis - */ -export async function getEmoji( - guildID: string, - emojiID: string, - addToCache = true, -) { - const result = await RequestManager.get( - endpoints.GUILD_EMOJI(guildID, emojiID), - ) as Emoji; - - if (addToCache) { - const guild = await cacheHandlers.get("guilds", guildID); - if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); - guild.emojis.push(result); - cacheHandlers.set( - "guilds", - guildID, - guild, - ); - } - - return result; -} - -/** Create a new role for the guild. Requires the MANAGE_ROLES permission. */ -export async function createGuildRole( - guildID: string, - options: CreateRoleOptions, - reason?: string, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - const result = await RequestManager.post( - endpoints.GUILD_ROLES(guildID), - { - ...options, - permissions: calculateBits(options?.permissions || []), - reason, - }, - ); - - const roleData = result as RoleData; - const role = await structures.createRole(roleData); - const guild = await cacheHandlers.get("guilds", guildID); - guild?.roles.set(role.id, role); - - return role; -} - -/** Edit a guild role. Requires the MANAGE_ROLES permission. */ -export async function editRole( - guildID: string, - id: string, - options: CreateRoleOptions, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - const result = await RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), { - ...options, - permissions: options.permissions - ? calculateBits(options.permissions) - : undefined, - }); - - return result; -} - -/** Delete a guild role. Requires the MANAGE_ROLES permission. */ -export async function deleteRole(guildID: string, id: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - const result = await RequestManager.delete(endpoints.GUILD_ROLE(guildID, id)); - - return result; -} - -/** Returns a list of role objects for the guild. -* -* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your roles will be cached in your guild.** -*/ -export async function getRoles(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - const result = await RequestManager.get(endpoints.GUILD_ROLES(guildID)); - - return result; -} - -/** Modify the positions of a set of role objects for the guild. Requires the MANAGE_ROLES permission. */ -export async function swapRoles(guildID: string, rolePositons: PositionSwap) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - const result = await RequestManager.patch( - endpoints.GUILD_ROLES(guildID), - rolePositons, - ); - - return result; -} - -/** Check how many members would be removed from the server in a prune operation. Requires the KICK_MEMBERS permission */ -export async function getPruneCount(guildID: string, options?: PruneOptions) { - if (options?.days && options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); - if (options?.days && options.days > 30) { - throw new Error(Errors.PRUNE_MAX_DAYS); - } - - const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_KICK_MEMBERS); - } - - const result = await RequestManager.get( - endpoints.GUILD_PRUNE(guildID), - { ...options, include_roles: options?.roles?.join(",") }, - ) as PrunePayload; - - return result.pruned; -} - -/** - * Begin a prune operation. Requires the KICK_MEMBERS permission. Returns an object with one 'pruned' key indicating the number of members that were removed in the prune operation. For large guilds it's recommended to set the computePruneCount option to false, forcing 'pruned' to null. Fires multiple Guild Member Remove Gateway events. - * - * By default, prune will not remove users with roles. You can optionally include specific roles in your prune by providing the roles (resolved to include_roles internally) parameter. Any inactive user that has a subset of the provided role(s) will be included in the prune and users with additional roles will not. - */ -export async function pruneMembers( - guildID: string, - { roles, computePruneCount, ...options }: PruneOptions, -) { - if (options.days && options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); - if (options.days && options.days > 30) throw new Error(Errors.PRUNE_MAX_DAYS); - - const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_KICK_MEMBERS); - } - - const result = await RequestManager.post( - endpoints.GUILD_PRUNE(guildID), - { - ...options, - compute_prune_count: computePruneCount, - include_roles: roles, - }, - ); - - return result; -} - -/** - * ⚠️ BEGINNER DEVS!! YOU SHOULD ALMOST NEVER NEED THIS AND YOU CAN GET FROM cache.members.get() - * - * ADVANCED: - * Highly recommended to use this function to fetch members instead of getMember from REST. - * REST: 50/s global(across all shards) rate limit with ALL requests this included - * GW(this function): 120/m(PER shard) rate limit. Meaning if you have 8 shards your limit is now 960/m. -*/ -export function fetchMembers(guild: Guild, options?: FetchMembersOptions) { - // You can request 1 member without the intent - if ( - (!options?.limit || options.limit > 1) && - !(identifyPayload.intents && Intents.GUILD_MEMBERS) - ) { - throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS); - } - - if (options?.userIDs?.length) { - options.limit = options.userIDs.length; - } - - return new Promise((resolve) => { - return requestAllMembers(guild, resolve, options); - }) as Promise>; -} - -/** - * ⚠️ BEGINNER DEVS!! YOU SHOULD ALMOST NEVER NEED THIS AND YOU CAN GET FROM cache.members.get() - * - * ADVANCED: - * Highly recommended to **NOT** use this function to get members instead use fetchMembers(). - * REST(this function): 50/s global(across all shards) rate limit with ALL requests this included - * GW(fetchMembers): 120/m(PER shard) rate limit. Meaning if you have 8 shards your limit is 960/m. -*/ -export async function getMembers( - guildID: string, - options?: GetMemberOptions, -) { - if (!(identifyPayload.intents && Intents.GUILD_MEMBERS)) { - throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS); - } - - const guild = await cacheHandlers.get("guilds", guildID); - if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); - - const members = new Collection(); - - let membersLeft = options?.limit ?? guild.memberCount; - let loops = 1; - while ( - (options?.limit ?? guild.memberCount) > members.size && membersLeft > 0 - ) { - if (options?.limit && options.limit > 1000) { - console.log( - `Paginating get members from REST. #${loops} / ${ - Math.ceil((options?.limit ?? 1) / 1000) - }`, - ); - } - - const result = await RequestManager.get( - `${endpoints.GUILD_MEMBERS(guildID)}?limit=${ - membersLeft > 1000 ? 1000 : membersLeft - }${options?.after ? `&after=${options.after}` : ""}`, - ) as MemberCreatePayload[]; - - const memberStructures = await Promise.all( - result.map(async (member) => { - const memberStruct = await structures.createMember(member, guildID); - - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - return memberStruct; - }), - ) as Member[]; - - if (!memberStructures.length) break; - - memberStructures.forEach((member) => members.set(member.id, member)); - - options = { - limit: options?.limit, - after: memberStructures[memberStructures.length - 1].id, - }; - - membersLeft -= 1000; - - loops++; - } - - return members; -} - -/** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */ -export async function getAuditLogs( - guildID: string, - options: GetAuditLogsOptions, -) { - const hasPerm = await botHasPermission(guildID, ["VIEW_AUDIT_LOG"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_VIEW_AUDIT_LOG); - } - - const result = await RequestManager.get(endpoints.GUILD_AUDIT_LOGS(guildID), { - ...options, - action_type: options.action_type - ? AuditLogs[options.action_type] - : undefined, - limit: options.limit && options.limit >= 1 && options.limit <= 100 - ? options.limit - : 50, - }); - - return result; -} - -/** Returns the guild embed object. Requires the MANAGE_GUILD permission. */ -export async function getEmbed(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } - - const result = await RequestManager.get(endpoints.GUILD_WIDGET(guildID)); - - return result; -} - -/** Modify a guild embed object for the guild. Requires the MANAGE_GUILD permission. */ -export async function editEmbed( - guildID: string, - enabled: boolean, - channelID?: string | null, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } - - const result = await RequestManager.patch( - endpoints.GUILD_WIDGET(guildID), - { enabled, channel_id: channelID }, - ); - - return result; -} - -/** Returns the code and uses of the vanity url for this server if it is enabled. Requires the MANAGE_GUILD permission. */ -export async function getVanityURL(guildID: string) { - const result = await RequestManager.get(endpoints.GUILD_VANITY_URL(guildID)); - - return result; -} - -/** Returns a list of integrations for the guild. Requires the MANAGE_GUILD permission. */ -export async function getIntegrations(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } - - const result = await RequestManager.get( - endpoints.GUILD_INTEGRATIONS(guildID), - ); - - return result; -} - -/** Modify the behavior and settings of an integration object for the guild. Requires the MANAGE_GUILD permission. */ -export async function editIntegration( - guildID: string, - id: string, - options: EditIntegrationOptions, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } - - const result = await RequestManager.patch( - endpoints.GUILD_INTEGRATION(guildID, id), - options, - ); - - return result; -} - -/** Delete the attached integration object for the guild with this id. Requires MANAGE_GUILD permission. */ -export async function deleteIntegration(guildID: string, id: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } - - const result = await RequestManager.delete( - endpoints.GUILD_INTEGRATION(guildID, id), - ); - - return result; -} - -/** Sync an integration. Requires the MANAGE_GUILD permission. */ -export async function syncIntegration(guildID: string, id: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } - - const result = await RequestManager.post( - endpoints.GUILD_INTEGRATION_SYNC(guildID, id), - ); - - return result; -} - -/** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */ -export async function getBans(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_BAN_MEMBERS); - } - - const results = await RequestManager.get( - endpoints.GUILD_BANS(guildID), - ) as BannedUser[]; - - return new Collection( - results.map((res) => [res.user.id, res]), - ); -} - -/** Returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. */ -export async function getBan(guildID: string, memberID: string) { - const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_BAN_MEMBERS); - } - - const result = await RequestManager.get( - endpoints.GUILD_BAN(guildID, memberID), - ); - - return result as BannedUser; -} - -/** Ban a user from the guild and optionally delete previous messages sent by the user. Requires the BAN_MEMBERS permission. */ -export async function ban(guildID: string, id: string, options: BanOptions) { - const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_BAN_MEMBERS); - } - - const result = await RequestManager.put( - endpoints.GUILD_BAN(guildID, id), - { ...options, delete_message_days: options.days }, - ); - - return result; -} - -/** Remove the ban for a user. Requires BAN_MEMBERS permission */ -export async function unban(guildID: string, id: string) { - const hasPerm = await botHasPermission(guildID, ["BAN_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_BAN_MEMBERS); - } - - const result = await RequestManager.delete(endpoints.GUILD_BAN(guildID, id)); - - return result; -} - -/** Returns the guild preview object for the given id. If the bot is not in the guild, then the guild must be Discoverable. */ -export async function getGuildPreview(guildID: string) { - const result = await RequestManager.get(endpoints.GUILD_PREVIEW(guildID)); - - return result; -} - -/** Modify a guilds settings. Requires the MANAGE_GUILD permission. */ -export async function editGuild(guildID: string, options: GuildEditOptions) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } - - if (options.icon && !options.icon.startsWith("data:image/")) { - options.icon = await urlToBase64(options.icon); - } - - if (options.banner && !options.banner.startsWith("data:image/")) { - options.banner = await urlToBase64(options.banner); - } - - if (options.splash && !options.splash.startsWith("data:image/")) { - options.splash = await urlToBase64(options.splash); - } - - const result = await RequestManager.patch( - endpoints.GUILDS_BASE(guildID), - options, - ); - - return result; -} - -/** Get all the invites for this guild. Requires MANAGE_GUILD permission */ -export async function getInvites(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_GUILD); - } - - const result = await RequestManager.get(endpoints.GUILD_INVITES(guildID)); - - return result; -} - -/** Leave a guild */ -export async function leaveGuild(guildID: string) { - const result = await RequestManager.delete(endpoints.GUILD_LEAVE(guildID)); - - return result; -} - -/** Returns an array of voice regions that can be used when creating servers. */ -export async function getAvailableVoiceRegions() { - const result = await RequestManager.get(endpoints.VOICE_REGIONS); - - return result; -} - -/** Returns a list of voice region objects for the guild. Unlike the similar /voice route, this returns VIP servers when the guild is VIP-enabled. */ -export async function getVoiceRegions(guildID: string) { - const result = await RequestManager.get(endpoints.GUILD_REGIONS(guildID)); - - return result; -} - -/** Returns a list of guild webhooks objects. Requires the MANAGE_WEBHOOKs permission. */ -export async function getWebhooks(guildID: string) { - const hasPerm = await botHasPermission( - guildID, - ["MANAGE_WEBHOOKS"], - ); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } - - const result = await RequestManager.get(endpoints.GUILD_WEBHOOKS(guildID)); - - return result; -} - -/** This function will return the raw user payload in the rare cases you need to fetch a user directly from the API. */ -export async function getUser(userID: string) { - const result = await RequestManager.get( - endpoints.USER(userID), - ); - - return result as UserPayload; -} - -/** - * ⚠️ **If you need this, you are probably doing something wrong. Always use cache.guilds.get() - * - * Advanced Devs: - * This function fetches a guild's data. This is not the same data as a GUILD_CREATE. - * So it does not cache the guild, you must do it manually. - * */ -export async function getGuild(guildID: string, counts = true) { - const result = await RequestManager.get( - endpoints.GUILDS_BASE(guildID), - { with_counts: counts }, - ); - - return result as UpdateGuildPayload; -} - -/** Returns the guild template if it exists */ -export async function getTemplate(templateCode: string) { - const result = await RequestManager.get( - endpoints.GUILD_TEMPLATE(templateCode), - ) as GuildTemplate; - const template = await structures.createTemplate(result); - - return template; -} - -/** - * Returns the guild template if it exists - * @deprecated will get removed in v11 use `getTemplate` instead - */ -export function getGuildTemplate( - guildID: string, - templateCode: string, -) { - return getTemplate(templateCode); -} - -/** - * Create a new guild based on a template - * NOTE: This endpoint can be used only by bots in less than 10 guilds. - */ -export async function createGuildFromTemplate( - templateCode: string, - data: CreateGuildFromTemplate, -) { - if (await cacheHandlers.size("guilds") >= 10) { - throw new Error( - "This function can only be used by bots in less than 10 guilds.", - ); - } - - if (data.icon) { - data.icon = await urlToBase64(data.icon); - } - - const result = await await RequestManager.post( - endpoints.GUILD_TEMPLATE(templateCode), - data, - ); - - return result as CreateGuildPayload; -} - -/** - * Returns an array of templates. - * Requires the `MANAGE_GUILD` permission. - */ -export async function getGuildTemplates(guildID: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); - - const templates = await RequestManager.get( - endpoints.GUILD_TEMPLATES(guildID), - ) as GuildTemplate[]; - - return templates.map((template) => structures.createTemplate(template)); -} - -/** - * Deletes a template from a guild. - * Requires the `MANAGE_GUILD` permission. - */ -export async function deleteGuildTemplate( - guildID: string, - templateCode: string, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); - - const deletedTemplate = await RequestManager.delete( - `${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`, - ) as GuildTemplate; - - return structures.createTemplate(deletedTemplate); -} - -/** - * Creates a template for the guild. - * Requires the `MANAGE_GUILD` permission. - * @param name name of the template (1-100 characters) - * @param description description for the template (0-120 characters - */ -export async function createGuildTemplate( - guildID: string, - data: CreateGuildTemplate, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); - - if (data.name.length < 1 || data.name.length > 100) { - throw new Error("The name can only be in between 1-100 characters."); - } - - if ( - data.description?.length && - data.description.length > 120 - ) { - throw new Error("The description can only be in between 0-120 characters."); - } - - const template = await RequestManager.post( - endpoints.GUILD_TEMPLATES(guildID), - data, - ) as GuildTemplate; - - return structures.createTemplate(template); -} - -/** - * Syncs the template to the guild's current state. - * Requires the `MANAGE_GUILD` permission. - */ -export async function syncGuildTemplate(guildID: string, templateCode: string) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); - - const template = await RequestManager.put( - `${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`, - ) as GuildTemplate; - - return structures.createTemplate(template); -} - -/** - * Edit a template's metadata. - * Requires the `MANAGE_GUILD` permission. - */ -export async function editGuildTemplate( - guildID: string, - templateCode: string, - data: EditGuildTemplate, -) { - const hasPerm = await botHasPermission(guildID, ["MANAGE_GUILD"]); - if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD); - - if (data.name?.length && (data.name.length < 1 || data.name.length > 100)) { - throw new Error("The name can only be in between 1-100 characters."); - } - - if ( - data.description?.length && - data.description.length > 120 - ) { - throw new Error("The description can only be in between 0-120 characters."); - } - - const template = await RequestManager.patch( - `${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`, - data, - ) as GuildTemplate; - - return structures.createTemplate(template); -} diff --git a/src/api/handlers/member.ts b/src/api/handlers/member.ts deleted file mode 100644 index 143e48f9c..000000000 --- a/src/api/handlers/member.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { botID } from "../../bot.ts"; -import { RequestManager } from "../../rest/request_manager.ts"; -import { - ChannelCreatePayload, - DMChannelCreatePayload, - EditMemberOptions, - Errors, - ImageFormats, - ImageSize, - MessageContent, -} from "../../types/mod.ts"; -import { endpoints } from "../../util/constants.ts"; -import { - botHasPermission, - higherRolePosition, - highestRole, -} from "../../util/permissions.ts"; -import { formatImageURL, urlToBase64 } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; -import { Member, structures } from "../structures/mod.ts"; -import { sendMessage } from "./channel.ts"; - -/** The users custom avatar or the default avatar if you don't have a member object. */ -export function rawAvatarURL( - userID: string, - discriminator: string, - avatar?: string | null, - size: ImageSize = 128, - format?: ImageFormats, -) { - return avatar - ? formatImageURL(endpoints.USER_AVATAR(userID, avatar), size, format) - : endpoints.USER_DEFAULT_AVATAR(Number(discriminator) % 5); -} - -/** The users custom avatar or the default avatar */ -export function avatarURL( - member: Member, - size: ImageSize = 128, - format?: ImageFormats, -) { - return rawAvatarURL( - member.id, - member.discriminator, - member.avatar, - size, - format, - ); -} - -/** Add a role to the member */ -export async function addRole( - guildID: string, - memberID: string, - roleID: string, - reason?: string, -) { - const botsHighestRole = await highestRole(guildID, botID); - if (botsHighestRole) { - const hasHigherRolePosition = await higherRolePosition( - guildID, - botsHighestRole.id, - roleID, - ); - if ( - !hasHigherRolePosition && - (await cacheHandlers.get("guilds", guildID))?.ownerID !== botID - ) { - throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); - } - } - - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - const result = await RequestManager.put( - endpoints.GUILD_MEMBER_ROLE(guildID, memberID, roleID), - { reason }, - ); - - return result; -} - -/** Remove a role from the member */ -export async function removeRole( - guildID: string, - memberID: string, - roleID: string, - reason?: string, -) { - const botsHighestRole = await highestRole(guildID, botID); - - if (botsHighestRole) { - const hasHigherRolePosition = await higherRolePosition( - guildID, - botsHighestRole.id, - roleID, - ); - if ( - !hasHigherRolePosition && - (await cacheHandlers.get("guilds", guildID))?.ownerID !== botID - ) { - throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); - } - } - - const hasPerm = await botHasPermission(guildID, ["MANAGE_ROLES"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - const result = await RequestManager.delete( - endpoints.GUILD_MEMBER_ROLE(guildID, memberID, roleID), - { reason }, - ); - - return result; -} - -/** Send a message to a users DM. Note: this takes 2 API calls. 1 is to fetch the users dm channel. 2 is to send a message to that channel. */ -export async function sendDirectMessage( - memberID: string, - content: string | MessageContent, -) { - let dmChannel = await cacheHandlers.get("channels", memberID); - if (!dmChannel) { - // If not available in cache create a new one. - const dmChannelData = await RequestManager.post( - endpoints.USER_DM, - { recipient_id: memberID }, - ) as DMChannelCreatePayload; - const channelStruct = await structures.createChannel( - dmChannelData as unknown as ChannelCreatePayload, - ); - // Recreate the channel and add it undert he users id - await cacheHandlers.set("channels", memberID, channelStruct); - dmChannel = channelStruct; - } - - // If it does exist try sending a message to this user - return sendMessage(dmChannel.id, content); -} - -/** Kick a member from the server */ -export async function kick(guildID: string, memberID: string, reason?: string) { - const botsHighestRole = await highestRole(guildID, botID); - const membersHighestRole = await highestRole(guildID, memberID); - if ( - botsHighestRole && membersHighestRole && - botsHighestRole.position <= membersHighestRole.position - ) { - throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); - } - - const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]); - if (!hasPerm) { - throw new Error(Errors.MISSING_KICK_MEMBERS); - } - - const result = await RequestManager.delete( - endpoints.GUILD_MEMBER(guildID, memberID), - { reason }, - ); - - return result; -} - -/** Edit the member */ -export async function editMember( - guildID: string, - memberID: string, - options: EditMemberOptions, -) { - if (options.nick) { - if (options.nick.length > 32) { - throw new Error(Errors.NICKNAMES_MAX_LENGTH); - } - - const hasManageNickPerm = await botHasPermission( - guildID, - ["MANAGE_NICKNAMES"], - ); - if (!hasManageNickPerm) { - throw new Error(Errors.MISSING_MANAGE_NICKNAMES); - } - } - - const hasManageRolesPerm = await botHasPermission( - guildID, - ["MANAGE_ROLES"], - ); - if ( - options.roles && - !hasManageRolesPerm - ) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - if (options.mute) { - const hasMuteMembersPerm = await botHasPermission( - guildID, - ["MUTE_MEMBERS"], - ); - // TODO: This should check if the member is in a voice channel - if ( - !hasMuteMembersPerm - ) { - throw new Error(Errors.MISSING_MUTE_MEMBERS); - } - } - - const hasDeafenMembersPerm = await botHasPermission( - guildID, - ["DEAFEN_MEMBERS"], - ); - if ( - options.deaf && - !hasDeafenMembersPerm - ) { - throw new Error(Errors.MISSING_DEAFEN_MEMBERS); - } - - // TODO: if channel id is provided check if the bot has CONNECT and MOVE in channel and current channel - - const result = await RequestManager.patch( - endpoints.GUILD_MEMBER(guildID, memberID), - options, - ); - - return result; -} - -/** - * Move a member from a voice channel to another. - * @param guildID the id of the guild which the channel exists in - * @param memberID the id of the member to move. - * @param channelID id of channel to move user to (if they are connected to voice) - */ -export function moveMember( - guildID: string, - memberID: string, - channelID: string, -) { - return editMember(guildID, memberID, { channel_id: channelID }); -} - -/** Kicks a member from a voice channel */ -export function kickFromVoiceChannel(guildID: string, memberID: string) { - return editMember(guildID, memberID, { channel_id: null }); -} - -/** Modifies the bot's username or avatar. - * NOTE: username: if changed may cause the bot's discriminator to be randomized. - */ -export async function editBotProfile(username?: string, botAvatarURL?: string) { - // Nothing was edited - if (!username && !botAvatarURL) return; - // Check username requirements if username was provided - if (username) { - if (username.length > 32) { - throw new Error(Errors.USERNAME_MAX_LENGTH); - } - if (username.length < 2) { - throw new Error(Errors.USERNAME_MIN_LENGTH); - } - if (["@", "#", ":", "```"].some((char) => username.includes(char))) { - throw new Error(Errors.USERNAME_INVALID_CHARACTER); - } - if (["discordtag", "everyone", "here"].includes(username)) { - throw new Error(Errors.USERNAME_INVALID_USERNAME); - } - } - - const avatar = botAvatarURL ? await urlToBase64(botAvatarURL) : undefined; - const result = await RequestManager.patch( - endpoints.USER_BOT, - { - username: username?.trim(), - avatar, - }, - ); - - return result; -} - -/** Edit the nickname of the bot in this guild */ -export async function editBotNickname( - guildID: string, - nickname: string | null, -) { - const hasPerm = await botHasPermission(guildID, ["CHANGE_NICKNAME"]); - if (!hasPerm) throw new Error(Errors.MISSING_CHANGE_NICKNAME); - - const response = await RequestManager.patch( - endpoints.USER_NICK(guildID), - { nick: nickname }, - ) as { nick: string }; - - return response.nick; -} diff --git a/src/api/handlers/message.ts b/src/api/handlers/message.ts deleted file mode 100644 index 24c6579bb..000000000 --- a/src/api/handlers/message.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { botID } from "../../bot.ts"; -import { RequestManager } from "../../rest/request_manager.ts"; -import { - DiscordGetReactionsParams, - Errors, - MessageContent, - MessageCreateOptions, - UserPayload, -} from "../../types/mod.ts"; -import { endpoints } from "../../util/constants.ts"; -import { botHasChannelPermissions } from "../../util/permissions.ts"; -import { delay } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; -import { Message, structures } from "../structures/mod.ts"; - -/** Delete a message with the channel id and message id only. */ -export async function deleteMessageByID( - channelID: string, - messageID: string, - reason?: string, - delayMilliseconds = 0, -) { - const message = await cacheHandlers.get("messages", messageID); - if (message) return deleteMessage(message, reason, delayMilliseconds); - - if (delayMilliseconds) await delay(delayMilliseconds); - - const result = await RequestManager.delete( - endpoints.CHANNEL_MESSAGE(channelID, messageID), - { reason }, - ); - - return result; -} - -/** Delete a message */ -export async function deleteMessage( - message: Message, - reason?: string, - delayMilliseconds = 0, -) { - if (message.author.id !== botID) { - // This needs to check the channels permission not the guild permission - const hasManageMessages = await botHasChannelPermissions( - message.channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessages - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } - } - - if (delayMilliseconds) await delay(delayMilliseconds); - - const result = await RequestManager.delete( - endpoints.CHANNEL_MESSAGE(message.channelID, message.id), - { reason }, - ); - - return result; -} - -/** Pin a message in a channel. Requires MANAGE_MESSAGES. Max pins allowed in a channel = 50. */ -export async function pin(channelID: string, messageID: string) { - const hasManageMessagesPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessagesPerm - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } - - const result = await RequestManager.put( - endpoints.CHANNEL_PIN(channelID, messageID), - ); - - return result; -} - -/** Unpin a message in a channel. Requires MANAGE_MESSAGES. */ -export async function unpin(channelID: string, messageID: string) { - const hasManageMessagesPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_MESSAGES"], - ); - if ( - !hasManageMessagesPerm - ) { - throw new Error(Errors.MISSING_MANAGE_MESSAGES); - } - - const result = await RequestManager.delete( - endpoints.CHANNEL_PIN(channelID, messageID), - ); - - return result; -} - -/** Create a reaction for the message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. Requires READ_MESSAGE_HISTORY and ADD_REACTIONS */ -export async function addReaction( - channelID: string, - messageID: string, - reaction: string, -) { - const hasAddReactionsPerm = await botHasChannelPermissions( - channelID, - ["ADD_REACTIONS"], - ); - if (!hasAddReactionsPerm) { - throw new Error(Errors.MISSING_ADD_REACTIONS); - } - - const hasReadMessageHistoryPerm = await botHasChannelPermissions( - channelID, - ["READ_MESSAGE_HISTORY"], - ); - if ( - !hasReadMessageHistoryPerm - ) { - throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); - } - - if (reaction.startsWith("<:")) { - reaction = reaction.substring(2, reaction.length - 1); - } else if (reaction.startsWith(" addReaction(channelID, messageID, reaction)), - ); - } else { - for (const reaction of reactions) { - await addReaction(channelID, messageID, reaction); - } - } -} - -/** Removes a reaction from the bot on this message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. */ -export async function removeReaction( - channelID: string, - messageID: string, - reaction: string, -) { - if (reaction.startsWith("<:")) { - reaction = reaction.substring(2, reaction.length - 1); - } else if (reaction.startsWith(" { - const member = await cacheHandlers.get("members", res.id); - return member || res; - })); -} - -/** Edit the message. */ -export async function editMessage( - message: Message, - content: string | MessageContent, -) { - if ( - message.author.id !== botID - ) { - throw "You can only edit a message that was sent by the bot."; - } - - if (typeof content === "string") content = { content }; - - const hasSendMessagesPerm = await botHasChannelPermissions( - message.channelID, - ["SEND_MESSAGES"], - ); - if ( - !hasSendMessagesPerm - ) { - throw new Error(Errors.MISSING_SEND_MESSAGES); - } - - const hasSendTtsMessagesPerm = await botHasChannelPermissions( - message.channelID, - ["SEND_TTS_MESSAGES"], - ); - if ( - content.tts && - !hasSendTtsMessagesPerm - ) { - throw new Error(Errors.MISSING_SEND_TTS_MESSAGE); - } - - if (content.content && content.content.length > 2000) { - throw new Error(Errors.MESSAGE_MAX_LENGTH); - } - - const result = await RequestManager.patch( - endpoints.CHANNEL_MESSAGE(message.channelID, message.id), - content, - ); - - return structures.createMessage(result as MessageCreateOptions); -} - -/** Crosspost a message in a News Channel to following channels. */ -export async function publishMessage(channelID: string, messageID: string) { - const data = await RequestManager.post( - endpoints.CHANNEL_MESSAGE_CROSSPOST(channelID, messageID), - ) as MessageCreateOptions; - - return structures.createMessage(data); -} diff --git a/src/api/handlers/mod.ts b/src/api/handlers/mod.ts deleted file mode 100644 index ddd9070a0..000000000 --- a/src/api/handlers/mod.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { - channelOverwriteHasPermission, - createInvite, - deleteInvite, - deleteMessages, - editChannel, - followChannel, - getChannelInvites, - getChannelWebhooks, - getInvite, - getMessage, - getMessages, - getPins, - isChannelSynced, - sendMessage, - startTyping, -} from "./channel.ts"; -import { getGatewayBot } from "./gateway.ts"; -import { - ban, - categoryChildrenIDs, - createEmoji, - createGuildChannel, - createGuildFromTemplate, - createGuildRole, - createGuildTemplate, - createServer, - deleteChannel, - deleteChannelOverwrite, - deleteEmoji, - deleteGuildTemplate, - deleteIntegration, - deleteRole, - deleteServer, - editChannelOverwrite, - editEmbed, - editEmoji, - editGuild, - editGuildTemplate, - editIntegration, - editRole, - emojiURL, - fetchMembers, - getAuditLogs, - getAvailableVoiceRegions, - getBan, - getBans, - getChannel, - getChannels, - getEmbed, - getEmoji, - getEmojis, - getGuild, - getGuildPreview, - getGuildTemplate, - getGuildTemplates, - getIntegrations, - getInvites, - getMember, - getMembers, - getMembersByQuery, - getPruneCount, - getRoles, - getTemplate, - getUser, - getVanityURL, - getVoiceRegions, - getWebhooks, - guildBannerURL, - guildIconURL, - guildSplashURL, - leaveGuild, - pruneMembers, - swapChannels, - swapRoles, - syncGuildTemplate, - syncIntegration, - unban, -} from "./guild.ts"; -import { - addRole, - avatarURL, - editBotNickname, - editBotProfile, - editMember, - kick, - kickFromVoiceChannel, - moveMember, - rawAvatarURL, - removeRole, - sendDirectMessage, -} from "./member.ts"; -import { - addReaction, - addReactions, - deleteMessage, - deleteMessageByID, - editMessage, - getReactions, - pin, - publishMessage, - removeAllReactions, - removeReaction, - removeReactionEmoji, - removeUserReaction, - unpin, -} from "./message.ts"; -import { getApplicationInformation } from "./oauth.ts"; -import { - createSlashCommand, - createWebhook, - deleteSlashCommand, - deleteSlashResponse, - deleteWebhookMessage, - editSlashCommand, - editSlashResponse, - editWebhookMessage, - executeSlashCommand, - executeWebhook, - getSlashCommand, - getSlashCommands, - getWebhook, - upsertSlashCommand, - upsertSlashCommands, -} from "./webhook.ts"; - -export let handlers = { - // Channel handler - channelOverwriteHasPermission, - createInvite, - deleteMessages, - editChannel, - followChannel, - getChannelInvites, - getChannelWebhooks, - getMessage, - getMessages, - getPins, - isChannelSynced, - sendMessage, - getInvite, - deleteInvite, - startTyping, - - // Gateway handler - getGatewayBot, - - // Guild handler - ban, - categoryChildrenIDs, - createEmoji, - createGuildChannel, - createGuildFromTemplate, - createGuildRole, - createGuildTemplate, - createServer, - deleteChannel, - deleteEmoji, - deleteGuildTemplate, - deleteIntegration, - deleteRole, - deleteServer, - editEmbed, - editEmoji, - editGuild, - editGuildTemplate, - editIntegration, - editRole, - emojiURL, - fetchMembers, - getAuditLogs, - getBan, - getBans, - getChannel, - getChannels, - getEmbed, - getEmoji, - getEmojis, - getGuild, - getGuildPreview, - getGuildTemplate, - getGuildTemplates, - getAvailableVoiceRegions, - getIntegrations, - getInvites, - getMember, - getMembers, - getTemplate, - getMembersByQuery, - getPruneCount, - getRoles, - getUser, - getVanityURL, - getVoiceRegions, - getWebhooks, - guildBannerURL, - guildIconURL, - guildSplashURL, - leaveGuild, - pruneMembers, - swapChannels, - editChannelOverwrite, - deleteChannelOverwrite, - swapRoles, - syncGuildTemplate, - syncIntegration, - unban, - - // Member handler - addRole, - avatarURL, - editBotProfile, - editBotNickname, - editMember, - kick, - moveMember, - rawAvatarURL, - removeRole, - sendDirectMessage, - kickFromVoiceChannel, - - // Message handler - addReaction, - addReactions, - deleteMessage, - deleteMessageByID, - editMessage, - getReactions, - pin, - publishMessage, - removeAllReactions, - removeReaction, - removeReactionEmoji, - removeUserReaction, - unpin, - - // Webhook handler - createWebhook, - executeWebhook, - getWebhook, - editWebhookMessage, - deleteWebhookMessage, - createSlashCommand, - getSlashCommand, - getSlashCommands, - upsertSlashCommand, - upsertSlashCommands, - editSlashCommand, - deleteSlashCommand, - executeSlashCommand, - deleteSlashResponse, - editSlashResponse, - - // OAuth handler - getApplicationInformation, -}; - -export type Handlers = typeof handlers; - -export function updateHandlers(newHandlers: Partial) { - handlers = { - ...handlers, - ...newHandlers, - }; -} diff --git a/src/api/handlers/oauth.ts b/src/api/handlers/oauth.ts deleted file mode 100644 index 2fed36909..000000000 --- a/src/api/handlers/oauth.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { RequestManager } from "../../rest/request_manager.ts"; -import { OAuthApplication } from "../../types/oauth.ts"; -import { endpoints } from "../../util/constants.ts"; - -/** Returns the bot's OAuth2 application object without `flags`. */ -export async function getApplicationInformation() { - const result = await RequestManager.get( - endpoints.OAUTH2_APPLICATION, - ); - - return result as OAuthApplication; -} diff --git a/src/api/handlers/webhook.ts b/src/api/handlers/webhook.ts deleted file mode 100644 index 1d2b56d8e..000000000 --- a/src/api/handlers/webhook.ts +++ /dev/null @@ -1,610 +0,0 @@ -import { applicationID } from "../../bot.ts"; -import { RequestManager } from "../../rest/request_manager.ts"; -import { - CreateSlashCommandOptions, - EditSlashCommandOptions, - EditSlashResponseOptions, - EditWebhookMessageOptions, - Errors, - ExecuteWebhookOptions, - MessageCreateOptions, - SlashCommand, - SlashCommandOption, - SlashCommandOptionChoice, - SlashCommandOptionType, - SlashCommandResponseOptions, - UpsertSlashCommandOptions, - UpsertSlashCommandsOptions, - WebhookCreateOptions, - WebhookEditOptions, - WebhookPayload, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { endpoints, SLASH_COMMANDS_NAME_REGEX } from "../../util/constants.ts"; -import { botHasChannelPermissions } from "../../util/permissions.ts"; -import { urlToBase64 } from "../../util/utils.ts"; -import { structures } from "../structures/mod.ts"; - -/** - * Create a new webhook. Requires the MANAGE_WEBHOOKS permission. Returns a webhook object on success. Webhook names follow our naming restrictions that can be found in our Usernames and Nicknames documentation, with the following additional stipulations: - * - * Webhook names cannot be: 'clyde' - */ -export async function createWebhook( - channelID: string, - options: WebhookCreateOptions, -) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } - - if ( - // Specific usernames that discord does not allow - options.name === "clyde" || - // Character limit checks. [...] checks are because of js unicode length handling - [...options.name].length < 2 || [...options.name].length > 32 - ) { - throw new Error(Errors.INVALID_WEBHOOK_NAME); - } - - const result = await RequestManager.post( - endpoints.CHANNEL_WEBHOOKS(channelID), - { - ...options, - avatar: options.avatar ? await urlToBase64(options.avatar) : undefined, - }, - ); - - return result as WebhookPayload; -} - -/** Edit a webhook. Requires the `MANAGE_WEBHOOKS` permission. Returns the updated webhook object on success. */ -export async function editWebhook( - channelID: string, - webhookID: string, - options: WebhookEditOptions, -) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } - - const result = await RequestManager.patch(endpoints.WEBHOOK_ID(webhookID), { - ...options, - channel_id: options.channelID, - }); - - return result as WebhookPayload; -} - -/** Edit a webhook. Returns the updated webhook object on success. */ -export async function editWebhookWithToken( - webhookID: string, - webhookToken: string, - options: Omit, -) { - const result = await RequestManager.patch( - endpoints.WEBHOOK(webhookID, webhookToken), - options, - ); - - return result as WebhookPayload; -} - -/** Delete a webhook permanently. Requires the `MANAGE_WEBHOOKS` permission. Returns a undefined on success */ -export async function deleteWebhook(channelID: string, webhookID: string) { - const hasManageWebhooksPerm = await botHasChannelPermissions( - channelID, - ["MANAGE_WEBHOOKS"], - ); - if ( - !hasManageWebhooksPerm - ) { - throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); - } - - const result = await RequestManager.delete(endpoints.WEBHOOK_ID(webhookID)); - - return result; -} - -/** Delete a webhook permanently. Returns a undefined on success */ -export async function deleteWebhookWithToken( - webhookID: string, - webhookToken: string, -) { - const result = await RequestManager.delete( - endpoints.WEBHOOK(webhookID, webhookToken), - ); - - return result; -} - -/** Returns the new webhook object for the given id. */ -export async function getWebhook(webhookID: string) { - const result = await RequestManager.get(endpoints.WEBHOOK_ID(webhookID)); - - return result as WebhookPayload; -} - -/** Returns the new webhook object for the given id, this call does not require authentication and returns no user in the webhook object. */ -export async function getWebhookWithToken(webhookID: string, token: string) { - const result = await RequestManager.get( - endpoints.WEBHOOK(webhookID, token), - ); - - return result as WebhookPayload; -} - -/** Execute a webhook with webhook ID and webhook token */ -export async function executeWebhook( - webhookID: string, - webhookToken: string, - options: ExecuteWebhookOptions, -) { - if (!options.content && !options.file && !options.embeds) { - throw new Error(Errors.INVALID_WEBHOOK_OPTIONS); - } - - if (options.content && options.content.length > 2000) { - throw Error(Errors.MESSAGE_MAX_LENGTH); - } - - if (options.embeds && options.embeds.length > 10) { - options.embeds.splice(10); - } - - if (options.mentions) { - if (options.mentions.users?.length) { - if (options.mentions.parse.includes("users")) { - options.mentions.parse = options.mentions.parse.filter((p) => - p !== "users" - ); - } - - if (options.mentions.users.length > 100) { - options.mentions.users = options.mentions.users.slice(0, 100); - } - } - - if (options.mentions.roles?.length) { - if (options.mentions.parse.includes("roles")) { - options.mentions.parse = options.mentions.parse.filter((p) => - p !== "roles" - ); - } - - if (options.mentions.roles.length > 100) { - options.mentions.roles = options.mentions.roles.slice(0, 100); - } - } - } - - const result = await RequestManager.post( - `${endpoints.WEBHOOK(webhookID, webhookToken)}${ - options.wait ? "?wait=true" : "" - }`, - { - ...options, - allowed_mentions: options.mentions, - avatar_url: options.avatar_url, - }, - ); - if (!options.wait) return; - - return structures.createMessage(result as MessageCreateOptions); -} - -export async function editWebhookMessage( - webhookID: string, - webhookToken: string, - messageID: string, - options: EditWebhookMessageOptions, -) { - if (options.content && options.content.length > 2000) { - throw Error(Errors.MESSAGE_MAX_LENGTH); - } - - if (options.embeds && options.embeds.length > 10) { - options.embeds.splice(10); - } - - if (options.allowed_mentions) { - if (options.allowed_mentions.users?.length) { - if (options.allowed_mentions.parse.includes("users")) { - options.allowed_mentions.parse = options.allowed_mentions.parse.filter(( - p, - ) => p !== "users"); - } - - if (options.allowed_mentions.users.length > 100) { - options.allowed_mentions.users = options.allowed_mentions.users.slice( - 0, - 100, - ); - } - } - - if (options.allowed_mentions.roles?.length) { - if (options.allowed_mentions.parse.includes("roles")) { - options.allowed_mentions.parse = options.allowed_mentions.parse.filter(( - p, - ) => p !== "roles"); - } - - if (options.allowed_mentions.roles.length > 100) { - options.allowed_mentions.roles = options.allowed_mentions.roles.slice( - 0, - 100, - ); - } - } - } - - const result = await RequestManager.patch( - endpoints.WEBHOOK_MESSAGE(webhookID, webhookToken, messageID), - { ...options, allowed_mentions: options.allowed_mentions }, - ) as MessageCreateOptions; - - const message = await structures.createMessage(result); - return message; -} - -export async function deleteWebhookMessage( - webhookID: string, - webhookToken: string, - messageID: string, -) { - const result = await RequestManager.delete( - endpoints.WEBHOOK_MESSAGE(webhookID, webhookToken, messageID), - ); - - return result; -} - -function validateSlashOptionChoices( - choices: SlashCommandOptionChoice[], - optionType: SlashCommandOptionType, -) { - for (const choice of choices) { - if ([...choice.name].length < 1 || [...choice.name].length > 100) { - throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); - } - - if ( - (optionType === SlashCommandOptionType.STRING && - (typeof choice.value !== "string" || choice.value.length < 1 || - choice.value.length > 100)) || - (optionType === SlashCommandOptionType.INTEGER && - typeof choice.value !== "number") - ) { - throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); - } - } -} - -function validateSlashOptions(options: SlashCommandOption[]) { - for (const option of options) { - if ( - (option.choices?.length && option.choices.length > 25) || - option.type !== SlashCommandOptionType.STRING && - option.type !== SlashCommandOptionType.INTEGER - ) { - throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); - } - - if ( - ([...option.name].length < 1 || [...option.name].length > 32) || - ([...option.description].length < 1 || - [...option.description].length > 100) - ) { - throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); - } - - if (option.choices) { - validateSlashOptionChoices(option.choices, option.type); - } - } -} - -function validateSlashCommands( - commands: UpsertSlashCommandOptions[], - create = false, -) { - for (const command of commands) { - if ( - (command.name && !SLASH_COMMANDS_NAME_REGEX.test(command.name)) || - (create && !command.name) - ) { - throw new Error(Errors.INVALID_SLASH_NAME); - } - - if ( - (command.description && - ([...command.description].length < 1 || - [...command.description].length > 100)) || - (create && !command.description) - ) { - throw new Error(Errors.INVALID_SLASH_DESCRIPTION); - } - - if (command.options?.length) { - if (command.options.length > 25) { - throw new Error(Errors.INVALID_SLASH_OPTIONS); - } - - validateSlashOptions(command.options); - } - } -} - -/** - * There are two kinds of Slash Commands: global commands and guild commands. Global commands are available for every guild that adds your app; guild commands are specific to the guild you specify when making them. Command names are unique per application within each scope (global and guild). That means: - * - * - Your app **cannot** have two global commands with the same name - * - Your app **cannot** have two guild commands within the same name **on the same guild** - * - Your app **can** have a global and guild command with the same name - * - Multiple apps **can** have commands with the same names - * - * Global commands are cached for **1 hour**. That means that new global commands will fan out slowly across all guilds, and will be guaranteed to be updated in an hour. - * Guild commands update **instantly**. We recommend you use guild commands for quick testing, and global commands when they're ready for public use. - */ -export async function createSlashCommand(options: CreateSlashCommandOptions) { - validateSlashCommands([options], true); - - const result = await RequestManager.post( - options.guildID - ? endpoints.COMMANDS_GUILD(applicationID, options.guildID) - : endpoints.COMMANDS(applicationID), - { - ...options, - }, - ); - - return result; -} - -/** Fetchs the global command for the given ID. If a guildID is provided, the guild command will be fetched. */ -export async function getSlashCommand(commandID: string, guildID?: string) { - const result = await RequestManager.get( - guildID - ? endpoints.COMMANDS_GUILD_ID(applicationID, guildID, commandID) - : endpoints.COMMANDS_ID(applicationID, commandID), - ); - - return result as SlashCommand; -} - -/** Fetch all of the global commands for your application. */ -export async function getSlashCommands(guildID?: string) { - // TODO: Should this be a returned as a collection? - const result = await RequestManager.get( - guildID - ? endpoints.COMMANDS_GUILD(applicationID, guildID) - : endpoints.COMMANDS(applicationID), - ); - - return result; -} - -/** - * Edit an existing slash command. If this command did not exist, it will create it. - */ -export async function upsertSlashCommand( - commandID: string, - options: UpsertSlashCommandOptions, - guildID?: string, -) { - validateSlashCommands([options]); - - const result = await RequestManager.patch( - guildID - ? endpoints.COMMANDS_GUILD_ID( - applicationID, - guildID, - commandID, - ) - : endpoints.COMMANDS_ID(applicationID, commandID), - options, - ); - - return result; -} - -/** - * Bulk edit existing slash commands. If a command does not exist, it will create it. - * - * **NOTE:** Any slash commands that are not specified in this function will be **deleted**. If you don't provide the commandID and rename your command, the command gets a new ID. - */ -export async function upsertSlashCommands( - options: UpsertSlashCommandsOptions[], - guildID?: string, -) { - validateSlashCommands(options); - - const result = await RequestManager.put( - guildID - ? endpoints.COMMANDS_GUILD(applicationID, guildID) - : endpoints.COMMANDS(applicationID), - options, - ); - - return result; -} - -// TODO: remove this function for v11 -/** - * Edit an existing slash command. - * @deprecated This function will be removed in v11. Use `upsertSlashCommand()` instead - */ -export async function editSlashCommand( - commandID: string, - options: EditSlashCommandOptions, - guildID?: string, -) { - if (!SLASH_COMMANDS_NAME_REGEX.test(options.name)) { - throw new Error(Errors.INVALID_SLASH_NAME); - } - - if ( - [...options.description].length < 1 || [...options.description].length > 100 - ) { - throw new Error(Errors.INVALID_SLASH_DESCRIPTION); - } - - const result = await RequestManager.patch( - guildID - ? endpoints.COMMANDS_GUILD_ID( - applicationID, - guildID, - commandID, - ) - : endpoints.COMMANDS_ID(applicationID, commandID), - options, - ); - - return result; -} - -/** Deletes a slash command. */ -export function deleteSlashCommand(id: string, guildID?: string) { - if (!guildID) { - return RequestManager.delete(endpoints.COMMANDS_ID(applicationID, id)); - } - return RequestManager.delete( - endpoints.COMMANDS_GUILD_ID(applicationID, guildID, id), - ); -} - -/** - * Send a response to a users slash command. The command data will have the id and token necessary to respond. - * Interaction `tokens` are valid for **15 minutes** and can be used to send followup messages. - * - * NOTE: By default we will suppress mentions. To enable mentions, just pass any mentions object. - */ -export async function executeSlashCommand( - id: string, - token: string, - options: SlashCommandResponseOptions, -) { - // If its already been executed, we need to send a followup response - if (cache.executedSlashCommands.has(token)) { - return RequestManager.post(endpoints.WEBHOOK(applicationID, token), { - ...options, - }); - } - - // Expire in 15 minutes - cache.executedSlashCommands.set(token, id); - setTimeout( - () => cache.executedSlashCommands.delete(token), - 900000, - ); - - // If the user wants this as a private message mark it ephemeral - if (options.private) { - options.data.flags = 64; - } - - // If no mentions are provided, force disable mentions - if (!(options.data.allowed_mentions)) { - options.data.allowed_mentions = { parse: [] }; - } - - const result = await RequestManager.post( - endpoints.INTERACTION_ID_TOKEN(id, token), - options, - ); - - return result; -} - -/** To delete your response to a slash command. If a message id is not provided, it will default to deleting the original response. */ -export async function deleteSlashResponse( - token: string, - messageID?: string, -) { - const result = await RequestManager.delete( - messageID - ? endpoints.INTERACTION_ID_TOKEN_MESSAGEID( - applicationID, - token, - messageID, - ) - : endpoints.INTERACTION_ORIGINAL_ID_TOKEN(applicationID, token), - ); - - return result; -} - -/** To edit your response to a slash command. If a messageID is not provided it will default to editing the original response. */ -export async function editSlashResponse( - token: string, - options: EditSlashResponseOptions, -) { - if (options.content && options.content.length > 2000) { - throw Error(Errors.MESSAGE_MAX_LENGTH); - } - - if (options.embeds && options.embeds.length > 10) { - options.embeds.splice(10); - } - - if (options.allowed_mentions) { - if (options.allowed_mentions.users?.length) { - if (options.allowed_mentions.parse.includes("users")) { - options.allowed_mentions.parse = options.allowed_mentions.parse.filter(( - p, - ) => p !== "users"); - } - - if (options.allowed_mentions.users.length > 100) { - options.allowed_mentions.users = options.allowed_mentions.users.slice( - 0, - 100, - ); - } - } - - if (options.allowed_mentions.roles?.length) { - if (options.allowed_mentions.parse.includes("roles")) { - options.allowed_mentions.parse = options.allowed_mentions.parse.filter(( - p, - ) => p !== "roles"); - } - - if (options.allowed_mentions.roles.length > 100) { - options.allowed_mentions.roles = options.allowed_mentions.roles.slice( - 0, - 100, - ); - } - } - } - - const result = await RequestManager.patch( - options.messageID - ? endpoints.WEBHOOK_MESSAGE(applicationID, token, options.messageID) - : endpoints.INTERACTION_ORIGINAL_ID_TOKEN(applicationID, token), - options, - ); - - // If the original message was edited, this will not return a message - if (!options.messageID) return result; - - const message = await structures.createMessage( - result as MessageCreateOptions, - ); - return message; -} diff --git a/src/api/structures/mod.ts b/src/api/structures/mod.ts deleted file mode 100644 index 8a2896232..000000000 --- a/src/api/structures/mod.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createChannel } from "./channel.ts"; -import { createGuild } from "./guild.ts"; -import { createMember } from "./member.ts"; -import { createMessage } from "./message.ts"; -import { createRole } from "./role.ts"; -import { createTemplate } from "./template.ts"; - -/** This is the placeholder where the structure creation functions are kept. */ -export let structures = { - createChannel, - createGuild, - createMember, - createMessage, - createRole, - createTemplate, -}; - -export type Structures = typeof structures; - -/** This function is used to update/reload/customize the internal structures of Discordeno. - * - * ⚠️ **ADVANCED USE ONLY: If you customize this incorrectly, you could potentially create many new errors/bugs. - * Please take caution when using this.** -*/ -export function updateStructures(newStructures: Structures) { - structures = { - ...structures, - ...newStructures, - }; -} - -export type { Channel } from "./channel.ts"; -export type { Guild } from "./guild.ts"; -export type { Member } from "./member.ts"; -export type { Message } from "./message.ts"; -export type { Role } from "./role.ts"; -export type { Template } from "./template.ts"; diff --git a/src/bot.ts b/src/bot.ts index 111f58426..3ea5fbfff 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,4 +1,4 @@ -import { getGatewayBot } from "./api/handlers/gateway.ts"; +import { getGatewayBot } from "./helpers/misc/get_gateway_bot.ts"; import { BotConfig, DiscordBotGatewayData, @@ -18,6 +18,7 @@ export let eventHandlers: EventHandlers = {}; export let botGatewayData: DiscordBotGatewayData; export let proxyWSURL = `wss://gateway.discord.gg`; +export let lastShardID = 0; export const identifyPayload: DiscordIdentify = { token: "", @@ -60,9 +61,10 @@ export async function startBot(config: BotConfig) { (bits, next) => (bits |= typeof next === "string" ? Intents[next] : next), 0, ); - identifyPayload.shard = [0, botGatewayData.shards]; + lastShardID = botGatewayData.shards; + identifyPayload.shard = [0, lastShardID]; - await spawnShards(botGatewayData, identifyPayload, 0, botGatewayData.shards); + await spawnShards(botGatewayData, identifyPayload, 0, lastShardID); } /** Allows you to dynamically update the event handlers by passing in new eventHandlers */ diff --git a/src/api/controllers/cache.ts b/src/cache.ts similarity index 94% rename from src/api/controllers/cache.ts rename to src/cache.ts index 0077c98e5..98c741d7e 100644 --- a/src/api/controllers/cache.ts +++ b/src/cache.ts @@ -1,9 +1,9 @@ // deno-lint-ignore-file require-await no-explicit-any prefer-const -import { PresenceUpdatePayload } from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { Collection } from "../../util/collection.ts"; -import { Channel, Guild, Member, Message } from "../structures/mod.ts"; +import { PresenceUpdatePayload } from "./types/mod.ts"; +import { cache } from "./util/cache.ts"; +import { Collection } from "./util/collection.ts"; +import { Channel, Guild, Member, Message } from "./structures/mod.ts"; export type TableName = | "guilds" diff --git a/src/handlers/READY.ts b/src/handlers/READY.ts new file mode 100644 index 000000000..0192c696b --- /dev/null +++ b/src/handlers/READY.ts @@ -0,0 +1,103 @@ +import { + eventHandlers, + lastShardID, + setApplicationID, + setBotID, +} from "../bot.ts"; +import { DiscordPayload, ReadyPayload } from "../types/discord.ts"; +import { cache } from "../util/cache.ts"; +import { delay } from "../util/utils.ts"; +import { allowNextShard, basicShards } from "../ws/mod.ts"; +import { initialMemberLoadQueue } from "../structures/guild.ts"; +import { structures } from "../structures/mod.ts"; +import { cacheHandlers } from "../cache.ts"; + +export async function handleReady( + data: DiscordPayload, + shardID: number, +) { + // The bot has already started, the last shard is resumed, however. + if (cache.isReady) return; + + const payload = data.d as ReadyPayload; + setBotID(payload.user.id); + setApplicationID(payload.application.id); + + // Triggered on each shard + eventHandlers.shardReady?.(shardID); + // Save when the READY event was received to prevent infinite load loops + const now = Date.now(); + + const shard = basicShards.get(shardID); + if (!shard) return; + + // Set ready to false just to go sure + shard.ready = false; + // All guilds are unavailable at first + shard.unavailableGuildIDs = new Set(payload.guilds.map((g) => g.id)); + + // Start ready check in 2 seconds + setTimeout(() => checkReady(payload, shardID, now), 2000); + + // Wait 5 seconds to spawn next shard + await delay(5000); + allowNextShard(); +} + +// Don't pass the shard itself because unavailableGuilds won't be updated by the GUILD_CREATE event +/** This function checks if the shard is fully loaded */ +function checkReady(payload: ReadyPayload, shardID: number, now: number) { + const shard = basicShards.get(shardID); + if (!shard) return; + + // Check if all guilds were loaded + if (shard.unavailableGuildIDs.size) { + if (Date.now() - now > 10000) { + eventHandlers.shardFailedToLoad?.(shardID, shard.unavailableGuildIDs); + // Force execute the loaded function to prevent infinite loop + loaded(shardID); + } else { + // Not all guilds were loaded but 10 seconds haven't passed so check again + setTimeout(() => checkReady(payload, shardID, now), 2000); + } + } else { + // All guilds were loaded + loaded(shardID); + } +} + +async function loaded(shardID: number) { + const shard = basicShards.get(shardID); + if (!shard) return; + + shard.ready = true; + + // If it is the last shard we can go full ready + if (shardID === lastShardID - 1) { + // Still some shards are loading so wait another 2 seconds for them + if (basicShards.some((shard) => !shard.ready)) { + setTimeout(() => loaded(shardID), 2000); + } else { + cache.isReady = true; + eventHandlers.ready?.(); + + // All the members that came in on guild creates should now be processed 1 by 1 + for (const [guildID, members] of initialMemberLoadQueue.entries()) { + await Promise.allSettled( + members.map(async (member) => { + const memberStruct = await structures.createMemberStruct( + member, + guildID, + ); + + return cacheHandlers.set( + "members", + memberStruct.id, + memberStruct, + ); + }), + ); + } + } + } +} diff --git a/src/handlers/channels/CHANNEL_CREATE.ts b/src/handlers/channels/CHANNEL_CREATE.ts new file mode 100644 index 000000000..d173000db --- /dev/null +++ b/src/handlers/channels/CHANNEL_CREATE.ts @@ -0,0 +1,13 @@ +import { eventHandlers } from "../../bot.ts"; +import { ChannelCreatePayload, DiscordPayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleChannelCreate(data: DiscordPayload) { + const payload = data.d as ChannelCreatePayload; + + const channelStruct = await structures.createChannelStruct(payload); + await cacheHandlers.set("channels", channelStruct.id, channelStruct); + + eventHandlers.channelCreate?.(channelStruct); +} diff --git a/src/api/controllers/channels.ts b/src/handlers/channels/CHANNEL_DELETE.ts similarity index 52% rename from src/api/controllers/channels.ts rename to src/handlers/channels/CHANNEL_DELETE.ts index 455d13bc7..0ddea96e2 100644 --- a/src/api/controllers/channels.ts +++ b/src/handlers/channels/CHANNEL_DELETE.ts @@ -4,23 +4,9 @@ import { ChannelTypes, DiscordPayload, } from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalChannelCreate(data: DiscordPayload) { - if (data.t !== "CHANNEL_CREATE") return; - - const payload = data.d as ChannelCreatePayload; - const channelStruct = await structures.createChannel(payload); - - await cacheHandlers.set("channels", channelStruct.id, channelStruct); - - eventHandlers.channelCreate?.(channelStruct); -} - -export async function handleInternalChannelDelete(data: DiscordPayload) { - if (data.t !== "CHANNEL_DELETE") return; +import { cacheHandlers } from "../../cache.ts"; +export async function handleChannelDelete(data: DiscordPayload) { const payload = data.d as ChannelCreatePayload; const cachedChannel = await cacheHandlers.get("channels", payload.id); @@ -52,17 +38,3 @@ export async function handleInternalChannelDelete(data: DiscordPayload) { }); eventHandlers.channelDelete?.(cachedChannel); } - -export async function handleInternalChannelUpdate(data: DiscordPayload) { - if (data.t !== "CHANNEL_UPDATE") return; - - const payload = data.d as ChannelCreatePayload; - const cachedChannel = await cacheHandlers.get("channels", payload.id); - const channelStruct = await structures.createChannel(payload); - - await cacheHandlers.set("channels", channelStruct.id, channelStruct); - - if (!cachedChannel) return; - - eventHandlers.channelUpdate?.(channelStruct, cachedChannel); -} diff --git a/src/handlers/channels/CHANNEL_PINS_UPDATE.ts b/src/handlers/channels/CHANNEL_PINS_UPDATE.ts new file mode 100644 index 000000000..a1d3f2c3e --- /dev/null +++ b/src/handlers/channels/CHANNEL_PINS_UPDATE.ts @@ -0,0 +1,19 @@ +import { eventHandlers } from "../../bot.ts"; +import { + DiscordChannelPinsUpdateEvent, + DiscordPayload, +} from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleChannelPinsUpdate(data: DiscordPayload) { + const payload = data.d as DiscordChannelPinsUpdateEvent; + + const channel = await cacheHandlers.get("channels", payload.channel_id); + if (!channel) return; + + const guild = payload.guild_id + ? await cacheHandlers.get("guilds", payload.guild_id) + : undefined; + + eventHandlers.channelPinsUpdate?.(channel, guild, payload.last_pin_timestamp); +} diff --git a/src/handlers/channels/CHANNEL_UPDATE.ts b/src/handlers/channels/CHANNEL_UPDATE.ts new file mode 100644 index 000000000..bed067acf --- /dev/null +++ b/src/handlers/channels/CHANNEL_UPDATE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../bot.ts"; +import { ChannelCreatePayload, DiscordPayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleChannelUpdate(data: DiscordPayload) { + const payload = data.d as ChannelCreatePayload; + const cachedChannel = await cacheHandlers.get("channels", payload.id); + + const channelStruct = await structures.createChannelStruct(payload); + await cacheHandlers.set("channels", channelStruct.id, channelStruct); + + if (!cachedChannel) return; + + eventHandlers.channelUpdate?.(channelStruct, cachedChannel); +} diff --git a/src/handlers/commands/APPLICATION_COMMAND_CREATE.ts b/src/handlers/commands/APPLICATION_COMMAND_CREATE.ts new file mode 100644 index 000000000..d60db03a9 --- /dev/null +++ b/src/handlers/commands/APPLICATION_COMMAND_CREATE.ts @@ -0,0 +1,18 @@ +import { eventHandlers } from "../../bot.ts"; +import { ApplicationCommandEvent, DiscordPayload } from "../../types/mod.ts"; + +export function handleApplicationCommandCreate( + data: DiscordPayload, +) { + const { + guild_id: guildID, + application_id: applicationID, + ...rest + } = data.d as ApplicationCommandEvent; + + eventHandlers.applicationCommandCreate?.({ + ...rest, + guildID, + applicationID, + }); +} diff --git a/src/handlers/commands/APPLICATION_COMMAND_DELETE.ts b/src/handlers/commands/APPLICATION_COMMAND_DELETE.ts new file mode 100644 index 000000000..6636003e1 --- /dev/null +++ b/src/handlers/commands/APPLICATION_COMMAND_DELETE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../bot.ts"; +import { ApplicationCommandEvent, DiscordPayload } from "../../types/mod.ts"; + +export function handleApplicationCommandDelete(data: DiscordPayload) { + const { + application_id: applicationID, + guild_id: guildID, + ...rest + } = data.d as ApplicationCommandEvent; + + eventHandlers.applicationCommandDelete?.({ + ...rest, + guildID, + applicationID, + }); +} diff --git a/src/handlers/commands/APPLICATION_COMMAND_UPDATE.ts b/src/handlers/commands/APPLICATION_COMMAND_UPDATE.ts new file mode 100644 index 000000000..5ac3fbde3 --- /dev/null +++ b/src/handlers/commands/APPLICATION_COMMAND_UPDATE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../bot.ts"; +import { ApplicationCommandEvent, DiscordPayload } from "../../types/mod.ts"; + +export function handleApplicationCommandUpdate(data: DiscordPayload) { + const { + application_id: applicationID, + guild_id: guildID, + ...rest + } = data.d as ApplicationCommandEvent; + + eventHandlers.applicationCommandUpdate?.({ + ...rest, + guildID, + applicationID, + }); +} diff --git a/src/handlers/emojis/GUILD_EMOJIS_UPDATE.ts b/src/handlers/emojis/GUILD_EMOJIS_UPDATE.ts new file mode 100644 index 000000000..9b6527cc6 --- /dev/null +++ b/src/handlers/emojis/GUILD_EMOJIS_UPDATE.ts @@ -0,0 +1,23 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildEmojisUpdatePayload } from "../../types/mod.ts"; +import { Collection } from "../../util/collection.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildEmojisUpdate(data: DiscordPayload) { + const payload = data.d as GuildEmojisUpdatePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const cachedEmojis = guild.emojis; + guild.emojis = new Collection( + payload.emojis.map((emoji) => [emoji.id ?? emoji.name, emoji]), + ); + + cacheHandlers.set("guilds", payload.guild_id, guild); + + eventHandlers.guildEmojisUpdate?.( + guild, + guild.emojis, + cachedEmojis, + ); +} diff --git a/src/handlers/guilds/GUILD_BAN_ADD.ts b/src/handlers/guilds/GUILD_BAN_ADD.ts new file mode 100644 index 000000000..4b77fa613 --- /dev/null +++ b/src/handlers/guilds/GUILD_BAN_ADD.ts @@ -0,0 +1,12 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildBanPayload } from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildBanAdd(data: DiscordPayload) { + const payload = data.d as GuildBanPayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const member = await cacheHandlers.get("members", payload.user.id); + eventHandlers.guildBanAdd?.(guild, payload.user, member); +} diff --git a/src/handlers/guilds/GUILD_BAN_REMOVE.ts b/src/handlers/guilds/GUILD_BAN_REMOVE.ts new file mode 100644 index 000000000..740791baf --- /dev/null +++ b/src/handlers/guilds/GUILD_BAN_REMOVE.ts @@ -0,0 +1,12 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildBanPayload } from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildBanRemove(data: DiscordPayload) { + const payload = data.d as GuildBanPayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const member = await cacheHandlers.get("members", payload.user.id); + eventHandlers.guildBanRemove?.(guild, payload.user, member); +} diff --git a/src/handlers/guilds/GUILD_CREATE.ts b/src/handlers/guilds/GUILD_CREATE.ts new file mode 100644 index 000000000..c43ccf9db --- /dev/null +++ b/src/handlers/guilds/GUILD_CREATE.ts @@ -0,0 +1,32 @@ +import { eventHandlers } from "../../bot.ts"; +import { cacheHandlers } from "../../cache.ts"; +import { structures } from "../../structures/mod.ts"; +import { CreateGuildPayload, DiscordPayload } from "../../types/mod.ts"; +import { cache } from "../../util/cache.ts"; +import { basicShards } from "../../ws/shard.ts"; + +export async function handleGuildCreate( + data: DiscordPayload, + shardID: number, +) { + const payload = data.d as CreateGuildPayload; + // When shards resume they emit GUILD_CREATE again. + if (await cacheHandlers.has("guilds", payload.id)) return; + + const guildStruct = await structures.createGuildStruct( + data.d as CreateGuildPayload, + shardID, + ); + await cacheHandlers.set("guilds", guildStruct.id, guildStruct); + + const shard = basicShards.get(shardID); + + if (shard?.unavailableGuildIDs.has(payload.id)) { + await cacheHandlers.delete("unavailableGuilds", payload.id); + + shard.unavailableGuildIDs.delete(payload.id); + } + + if (!cache.isReady) return eventHandlers.guildLoaded?.(guildStruct); + eventHandlers.guildCreate?.(guildStruct); +} diff --git a/src/handlers/guilds/GUILD_DELETE.ts b/src/handlers/guilds/GUILD_DELETE.ts new file mode 100644 index 000000000..1c5d5ed94 --- /dev/null +++ b/src/handlers/guilds/GUILD_DELETE.ts @@ -0,0 +1,49 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildDeletePayload } from "../../types/mod.ts"; +import { basicShards } from "../../ws/shard.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildDelete( + data: DiscordPayload, + shardID: number, +) { + const payload = data.d as GuildDeletePayload; + cacheHandlers.forEach("messages", (message) => { + if (message.guildID === payload.id) { + cacheHandlers.delete("messages", message.id); + } + }); + + cacheHandlers.forEach("channels", (channel) => { + if (channel.guildID === payload.id) { + cacheHandlers.delete("channels", channel.id); + } + }); + + cacheHandlers.forEach("members", async (member) => { + if (!member.guilds.has(payload.id)) return; + + member.guilds.delete(payload.id); + + if (!member.guilds.size) { + await cacheHandlers.delete("members", member.id); + return; + } + + await cacheHandlers.set("members", member.id, member); + }); + + if (payload.unavailable) { + const shard = basicShards.get(shardID); + if (shard) shard.unavailableGuildIDs.add(payload.id); + + return cacheHandlers.set("unavailableGuilds", payload.id, Date.now()); + } + + const guild = await cacheHandlers.get("guilds", payload.id); + if (!guild) return; + + await cacheHandlers.delete("guilds", payload.id); + + eventHandlers.guildDelete?.(guild); +} diff --git a/src/handlers/guilds/GUILD_INTEGRATIONS_UPDATE.ts b/src/handlers/guilds/GUILD_INTEGRATIONS_UPDATE.ts new file mode 100644 index 000000000..560b64de9 --- /dev/null +++ b/src/handlers/guilds/GUILD_INTEGRATIONS_UPDATE.ts @@ -0,0 +1,17 @@ +import { eventHandlers } from "../../bot.ts"; +import { + DiscordGuildIntegrationsUpdateEvent, + DiscordPayload, +} from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildIntegrationsUpdate( + data: DiscordPayload, +) { + const payload = data.d as DiscordGuildIntegrationsUpdateEvent; + + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + eventHandlers.guildIntegrationsUpdate?.(guild); +} diff --git a/src/handlers/guilds/GUILD_UPDATE.ts b/src/handlers/guilds/GUILD_UPDATE.ts new file mode 100644 index 000000000..c35dcdfa6 --- /dev/null +++ b/src/handlers/guilds/GUILD_UPDATE.ts @@ -0,0 +1,48 @@ +import { eventHandlers } from "../../bot.ts"; +import { + DiscordPayload, + GuildUpdateChange, + UpdateGuildPayload, +} from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildUpdate(data: DiscordPayload) { + const payload = data.d as UpdateGuildPayload; + const cachedGuild = await cacheHandlers.get("guilds", payload.id); + if (!cachedGuild) return; + + const keysToSkip = [ + "roles", + "guild_hashes", + "guild_id", + "max_members", + "emojis", + ]; + + const changes = Object.entries(payload) + .map(([key, value]) => { + if (keysToSkip.includes(key)) return; + + // @ts-ignore index signature + const cachedValue = cachedGuild[key]; + if (cachedValue !== value) { + // Guild create sends undefined and update sends false. + if (!cachedValue && !value) return; + + if (Array.isArray(cachedValue) && Array.isArray(value)) { + const different = (cachedValue.length !== value.length) || + cachedValue.find((val) => !value.includes(val)) || + value.find((val) => !cachedValue.includes(val)); + if (!different) return; + } + + // @ts-ignore index signature + cachedGuild[key] = value; + return { key, oldValue: cachedValue, value }; + } + }).filter((change) => change) as GuildUpdateChange[]; + + await cacheHandlers.set("guilds", payload.id, cachedGuild); + + eventHandlers.guildUpdate?.(cachedGuild, changes); +} diff --git a/src/handlers/integrations/INTEGRATION_CREATE.ts b/src/handlers/integrations/INTEGRATION_CREATE.ts new file mode 100644 index 000000000..7987653fd --- /dev/null +++ b/src/handlers/integrations/INTEGRATION_CREATE.ts @@ -0,0 +1,31 @@ +import { eventHandlers } from "../../bot.ts"; +import { + DiscordPayload, + IntegrationCreateUpdateEvent, +} from "../../types/mod.ts"; + +export function handleIntegrationCreate( + data: DiscordPayload, +) { + const { + guild_id: guildID, + enable_emoticons: enableEmoticons, + expire_behavior: expireBehavior, + expire_grace_period: expireGracePeriod, + subscriber_count: subscriberCount, + role_id: roleID, + synced_at: syncedAt, + ...rest + } = data.d as IntegrationCreateUpdateEvent; + + eventHandlers.integrationCreate?.({ + ...rest, + guildID, + enableEmoticons, + expireBehavior, + expireGracePeriod, + syncedAt, + subscriberCount, + roleID, + }); +} diff --git a/src/handlers/integrations/INTEGRATION_DELETE.ts b/src/handlers/integrations/INTEGRATION_DELETE.ts new file mode 100644 index 000000000..6b5d0fc74 --- /dev/null +++ b/src/handlers/integrations/INTEGRATION_DELETE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, IntegrationDeleteEvent } from "../../types/mod.ts"; + +export function handleIntegrationDelete(data: DiscordPayload) { + const { + guild_id: guildID, + application_id: applicationID, + ...rest + } = data.d as IntegrationDeleteEvent; + + eventHandlers.integrationDelete?.({ + ...rest, + applicationID, + guildID, + }); +} diff --git a/src/handlers/integrations/INTEGRATION_UPDATE.ts b/src/handlers/integrations/INTEGRATION_UPDATE.ts new file mode 100644 index 000000000..896088c17 --- /dev/null +++ b/src/handlers/integrations/INTEGRATION_UPDATE.ts @@ -0,0 +1,29 @@ +import { eventHandlers } from "../../bot.ts"; +import { + DiscordPayload, + IntegrationCreateUpdateEvent, +} from "../../types/mod.ts"; + +export function handleIntegrationUpdate(data: DiscordPayload) { + const { + enable_emoticons: enableEmoticons, + expire_behavior: expireBehavior, + expire_grace_period: expireGracePeriod, + role_id: roleID, + subscriber_count: subscriberCount, + synced_at: syncedAt, + guild_id: guildID, + ...rest + } = data.d as IntegrationCreateUpdateEvent; + + eventHandlers.integrationUpdate?.({ + ...rest, + guildID, + subscriberCount, + enableEmoticons, + expireGracePeriod, + roleID, + expireBehavior, + syncedAt, + }); +} diff --git a/src/handlers/interactions/INTERACTION_CREATE.ts b/src/handlers/interactions/INTERACTION_CREATE.ts new file mode 100644 index 000000000..ffdd6bc45 --- /dev/null +++ b/src/handlers/interactions/INTERACTION_CREATE.ts @@ -0,0 +1,20 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, InteractionCommandPayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleInteractionCreate(data: DiscordPayload) { + const payload = data.d as InteractionCommandPayload; + const memberStruct = await structures.createMemberStruct( + payload.member, + payload.guild_id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + eventHandlers.interactionCreate?.( + { + ...payload, + member: memberStruct, + }, + ); +} diff --git a/src/handlers/invites/INVITE_CREATE.ts b/src/handlers/invites/INVITE_CREATE.ts new file mode 100644 index 000000000..1b343bffa --- /dev/null +++ b/src/handlers/invites/INVITE_CREATE.ts @@ -0,0 +1,28 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, InviteCreateEvent } from "../../types/mod.ts"; + +export function handleInviteCreate(payload: DiscordPayload) { + if (payload.t !== "INVITE_CREATE") return; + //TODO: replace with tocamelcase + const { + channel_id: channelID, + created_at: createdAt, + max_age: maxAge, + guild_id: guildID, + target_user: targetUser, + target_user_type: targetUserType, + max_uses: maxUses, + ...rest + } = payload.d as InviteCreateEvent; + + eventHandlers.inviteCreate?.({ + ...rest, + channelID, + guildID, + maxAge, + targetUser, + targetUserType, + maxUses, + createdAt, + }); +} diff --git a/src/handlers/invites/INVITE_DELETE.ts b/src/handlers/invites/INVITE_DELETE.ts new file mode 100644 index 000000000..737fd9255 --- /dev/null +++ b/src/handlers/invites/INVITE_DELETE.ts @@ -0,0 +1,18 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, InviteDeleteEvent } from "../../types/mod.ts"; + +export function handleInviteDelete(payload: DiscordPayload) { + if (payload.t !== "INVITE_DELETE") return; + + const { + channel_id: channelID, + guild_id: guildID, + ...rest + } = payload.d as InviteDeleteEvent; + + eventHandlers.inviteDelete?.({ + ...rest, + channelID, + guildID, + }); +} diff --git a/src/handlers/members/GUILD_MEMBERS_CHUNK.ts b/src/handlers/members/GUILD_MEMBERS_CHUNK.ts new file mode 100644 index 000000000..bb2bf8702 --- /dev/null +++ b/src/handlers/members/GUILD_MEMBERS_CHUNK.ts @@ -0,0 +1,44 @@ +import { DiscordPayload, GuildMemberChunkPayload } from "../../types/mod.ts"; +import { cache } from "../../util/cache.ts"; +import { Collection } from "../../util/collection.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildMembersChunk(data: DiscordPayload) { + const payload = data.d as GuildMemberChunkPayload; + + const members = await Promise.all( + payload.members.map(async (member) => { + const memberStruct = await structures.createMemberStruct( + member, + payload.guild_id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + return memberStruct; + }), + ); + + // Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming + if ( + payload.nonce + ) { + const resolve = cache.fetchAllMembersProcessingRequests.get(payload.nonce); + if (!resolve) return; + + if (payload.chunk_index + 1 === payload.chunk_count) { + cache.fetchAllMembersProcessingRequests.delete(payload.nonce); + // Only 1 chunk most likely is all members or users only request a small amount of users + if (payload.chunk_count === 1) { + return resolve(new Collection(members.map((m) => [m.id, m]))); + } + + return resolve( + await cacheHandlers.filter( + "members", + (m) => m.guilds.has(payload.guild_id), + ), + ); + } + } +} diff --git a/src/handlers/members/GUILD_MEMBER_ADD.ts b/src/handlers/members/GUILD_MEMBER_ADD.ts new file mode 100644 index 000000000..c90f564e5 --- /dev/null +++ b/src/handlers/members/GUILD_MEMBER_ADD.ts @@ -0,0 +1,19 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildMemberAddPayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildMemberAdd(data: DiscordPayload) { + const payload = data.d as GuildMemberAddPayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + guild.memberCount++; + const memberStruct = await structures.createMemberStruct( + payload, + payload.guild_id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + eventHandlers.guildMemberAdd?.(guild, memberStruct); +} diff --git a/src/handlers/members/GUILD_MEMBER_REMOVE.ts b/src/handlers/members/GUILD_MEMBER_REMOVE.ts new file mode 100644 index 000000000..8c5703257 --- /dev/null +++ b/src/handlers/members/GUILD_MEMBER_REMOVE.ts @@ -0,0 +1,18 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildBanPayload } from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildMemberRemove(data: DiscordPayload) { + const payload = data.d as GuildBanPayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + guild.memberCount--; + const member = await cacheHandlers.get("members", payload.user.id); + eventHandlers.guildMemberRemove?.(guild, payload.user, member); + + member?.guilds.delete(guild.id); + if (member && !member.guilds.size) { + await cacheHandlers.delete("members", member.id); + } +} diff --git a/src/handlers/members/GUILD_MEMBER_UPDATE.ts b/src/handlers/members/GUILD_MEMBER_UPDATE.ts new file mode 100644 index 000000000..d63cb42ab --- /dev/null +++ b/src/handlers/members/GUILD_MEMBER_UPDATE.ts @@ -0,0 +1,59 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildMemberUpdatePayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildMemberUpdate(data: DiscordPayload) { + const payload = data.d as GuildMemberUpdatePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const cachedMember = await cacheHandlers.get("members", payload.user.id); + const guildMember = cachedMember?.guilds.get(payload.guild_id); + + const newMemberData = { + ...payload, + // deno-lint-ignore camelcase + premium_since: payload.premium_since || undefined, + // deno-lint-ignore camelcase + joined_at: new Date(guildMember?.joinedAt || Date.now()) + .toISOString(), + deaf: guildMember?.deaf || false, + mute: guildMember?.mute || false, + roles: payload.roles, + }; + const memberStruct = await structures.createMemberStruct( + newMemberData, + payload.guild_id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + if (guildMember?.nick !== payload.nick) { + eventHandlers.nicknameUpdate?.( + guild, + memberStruct, + payload.nick, + guildMember?.nick, + ); + } + + if (payload.pending === false && guildMember?.pending === true) { + eventHandlers.membershipScreeningPassed?.(guild, memberStruct); + } + + const roleIDs = guildMember?.roles || []; + + roleIDs.forEach((id) => { + if (!payload.roles.includes(id)) { + eventHandlers.roleLost?.(guild, memberStruct, id); + } + }); + + payload.roles.forEach((id) => { + if (!roleIDs.includes(id)) { + eventHandlers.roleGained?.(guild, memberStruct, id); + } + }); + + eventHandlers.guildMemberUpdate?.(guild, memberStruct, cachedMember); +} diff --git a/src/handlers/messages/MESSAGE_CREATE.ts b/src/handlers/messages/MESSAGE_CREATE.ts new file mode 100644 index 000000000..30337662e --- /dev/null +++ b/src/handlers/messages/MESSAGE_CREATE.ts @@ -0,0 +1,41 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, MessageCreateOptions } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleMessageCreate(data: DiscordPayload) { + const payload = data.d as MessageCreateOptions; + const channel = await cacheHandlers.get("channels", payload.channel_id); + if (channel) channel.lastMessageID = payload.id; + + const guild = payload.guild_id + ? await cacheHandlers.get("guilds", payload.guild_id) + : undefined; + + if (payload.member && guild) { + // If in a guild cache the author as a member + const memberStruct = await structures.createMemberStruct( + { ...payload.member, user: payload.author }, + guild.id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + } + + await Promise.all(payload.mentions.map(async (mention) => { + // Cache the member if its a valid member + if (mention.member && guild) { + const memberStruct = await structures.createMemberStruct( + { ...mention.member, user: mention }, + guild.id, + ); + + return cacheHandlers.set("members", memberStruct.id, memberStruct); + } + })); + + const message = await structures.createMessageStruct(payload); + // Cache the message + await cacheHandlers.set("messages", payload.id, message); + + eventHandlers.messageCreate?.(message); +} diff --git a/src/handlers/messages/MESSAGE_DELETE.ts b/src/handlers/messages/MESSAGE_DELETE.ts new file mode 100644 index 000000000..43a44a14a --- /dev/null +++ b/src/handlers/messages/MESSAGE_DELETE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, MessageDeletePayload } from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleMessageDelete(data: DiscordPayload) { + const payload = data.d as MessageDeletePayload; + const channel = await cacheHandlers.get("channels", payload.channel_id); + if (!channel) return; + + eventHandlers.messageDelete?.( + { id: payload.id, channel }, + await cacheHandlers.get("messages", payload.id), + ); + + await cacheHandlers.delete("messages", payload.id); +} diff --git a/src/handlers/messages/MESSAGE_DELETE_BULK.ts b/src/handlers/messages/MESSAGE_DELETE_BULK.ts new file mode 100644 index 000000000..ddb227ce1 --- /dev/null +++ b/src/handlers/messages/MESSAGE_DELETE_BULK.ts @@ -0,0 +1,17 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, MessageDeleteBulkPayload } from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleMessageDeleteBulk(data: DiscordPayload) { + const payload = data.d as MessageDeleteBulkPayload; + const channel = await cacheHandlers.get("channels", payload.channel_id); + if (!channel) return; + + return Promise.all(payload.ids.map(async (id) => { + eventHandlers.messageDelete?.( + { id, channel }, + await cacheHandlers.get("messages", id), + ); + await cacheHandlers.delete("messages", id); + })); +} diff --git a/src/handlers/messages/MESSAGE_REACTION_ADD.ts b/src/handlers/messages/MESSAGE_REACTION_ADD.ts new file mode 100644 index 000000000..e2cfc56c6 --- /dev/null +++ b/src/handlers/messages/MESSAGE_REACTION_ADD.ts @@ -0,0 +1,56 @@ +import { botID, eventHandlers } from "../../bot.ts"; +import { DiscordPayload, MessageReactionPayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleMessageReactionAdd(data: DiscordPayload) { + const payload = data.d as MessageReactionPayload; + const message = await cacheHandlers.get("messages", payload.message_id); + + if (message) { + const reactionExisted = message.reactions?.find( + (reaction) => + reaction.emoji.id === payload.emoji.id && + reaction.emoji.name === payload.emoji.name, + ); + + if (reactionExisted) reactionExisted.count++; + else { + const newReaction = { + count: 1, + me: payload.user_id === botID, + emoji: { ...payload.emoji, id: payload.emoji.id || undefined }, + }; + message.reactions = message.reactions + ? [...message.reactions, newReaction] + : [newReaction]; + } + + await cacheHandlers.set("messages", payload.message_id, message); + } + + if (payload.member && payload.guild_id) { + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (guild) { + const memberStruct = await structures.createMemberStruct( + payload.member, + guild.id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + } + } + + const uncachedOptions = { + ...payload, + id: payload.message_id, + channelID: payload.channel_id, + guildID: payload.guild_id || "", + }; + + eventHandlers.reactionAdd?.( + uncachedOptions, + payload.emoji, + payload.user_id, + message, + ); +} diff --git a/src/handlers/messages/MESSAGE_REACTION_REMOVE.ts b/src/handlers/messages/MESSAGE_REACTION_REMOVE.ts new file mode 100644 index 000000000..e0e77ec55 --- /dev/null +++ b/src/handlers/messages/MESSAGE_REACTION_REMOVE.ts @@ -0,0 +1,58 @@ +import { botID, eventHandlers } from "../../bot.ts"; +import { DiscordPayload, MessageReactionPayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleMessageReactionRemove( + data: DiscordPayload, +) { + const payload = data.d as MessageReactionPayload; + const message = await cacheHandlers.get("messages", payload.message_id); + + if (message) { + const reactionExisted = message.reactions?.find( + (reaction) => + reaction.emoji.id === payload.emoji.id && + reaction.emoji.name === payload.emoji.name, + ); + + if (reactionExisted) reactionExisted.count--; + else { + const newReaction = { + count: 1, + me: payload.user_id === botID, + emoji: { ...payload.emoji, id: payload.emoji.id || undefined }, + }; + message.reactions = message.reactions + ? [...message.reactions, newReaction] + : [newReaction]; + } + + await cacheHandlers.set("messages", payload.message_id, message); + } + + if (payload.member && payload.guild_id) { + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (guild) { + const memberStruct = await structures.createMemberStruct( + payload.member, + guild.id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + } + } + + const uncachedOptions = { + ...payload, + id: payload.message_id, + channelID: payload.channel_id, + guildID: payload.guild_id, + }; + + eventHandlers.reactionRemove?.( + uncachedOptions, + payload.emoji, + payload.user_id, + message, + ); +} diff --git a/src/handlers/messages/MESSAGE_REACTION_REMOVE_ALL.ts b/src/handlers/messages/MESSAGE_REACTION_REMOVE_ALL.ts new file mode 100644 index 000000000..8209abefe --- /dev/null +++ b/src/handlers/messages/MESSAGE_REACTION_REMOVE_ALL.ts @@ -0,0 +1,18 @@ +import { eventHandlers } from "../../bot.ts"; +import { BaseMessageReactionPayload, DiscordPayload } from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleMessageReactionRemoveAll( + data: DiscordPayload, +) { + const payload = data.d as BaseMessageReactionPayload; + const message = await cacheHandlers.get("messages", payload.message_id); + + if (message?.reactions) { + message.reactions = undefined; + + await cacheHandlers.set("messages", payload.message_id, message); + } + + eventHandlers.reactionRemoveAll?.(data.d as BaseMessageReactionPayload); +} diff --git a/src/handlers/messages/MESSAGE_REACTION_REMOVE_EMOJI.ts b/src/handlers/messages/MESSAGE_REACTION_REMOVE_EMOJI.ts new file mode 100644 index 000000000..87c9bec21 --- /dev/null +++ b/src/handlers/messages/MESSAGE_REACTION_REMOVE_EMOJI.ts @@ -0,0 +1,29 @@ +import { botID, eventHandlers } from "../../bot.ts"; +import { + DiscordPayload, + MessageReactionRemoveEmojiPayload, +} from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleMessageReactionRemoveEmoji( + data: DiscordPayload, +) { + const payload = data.d as MessageReactionRemoveEmojiPayload; + const message = await cacheHandlers.get("messages", payload.message_id); + + if (message?.reactions) { + message.reactions = message.reactions?.filter( + (reaction) => + !( + reaction.emoji.id === payload.emoji.id && + reaction.emoji.name === payload.emoji.name + ), + ); + + await cacheHandlers.set("messages", payload.message_id, message); + } + + eventHandlers.reactionRemoveEmoji?.( + data.d as MessageReactionRemoveEmojiPayload, + ); +} diff --git a/src/handlers/messages/MESSAGE_UPDATE.ts b/src/handlers/messages/MESSAGE_UPDATE.ts new file mode 100644 index 000000000..e4fc8e02c --- /dev/null +++ b/src/handlers/messages/MESSAGE_UPDATE.ts @@ -0,0 +1,36 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, MessageCreateOptions } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleMessageUpdate(data: DiscordPayload) { + const payload = data.d as MessageCreateOptions; + const channel = await cacheHandlers.get("channels", payload.channel_id); + if (!channel) return; + + const cachedMessage = await cacheHandlers.get("messages", payload.id); + if (!cachedMessage) return; + + const oldMessage = { + attachments: cachedMessage.attachments, + content: cachedMessage.content, + embeds: cachedMessage.embeds, + editedTimestamp: cachedMessage.editedTimestamp, + tts: cachedMessage.tts, + pinned: cachedMessage.pinned, + }; + + // Messages with embeds can trigger update but they wont have edited_timestamp + if ( + !payload.edited_timestamp || + (cachedMessage.content === payload.content) + ) { + return; + } + + const message = await structures.createMessageStruct(payload); + + await cacheHandlers.set("messages", payload.id, message); + + eventHandlers.messageUpdate?.(message, oldMessage); +} diff --git a/src/handlers/mod.ts b/src/handlers/mod.ts new file mode 100644 index 000000000..0eb9fc2f7 --- /dev/null +++ b/src/handlers/mod.ts @@ -0,0 +1,149 @@ +import { handleChannelCreate } from "./channels/CHANNEL_CREATE.ts"; +import { handleChannelDelete } from "./channels/CHANNEL_DELETE.ts"; +import { handleChannelPinsUpdate } from "./channels/CHANNEL_PINS_UPDATE.ts"; +import { handleChannelUpdate } from "./channels/CHANNEL_UPDATE.ts"; +import { handleApplicationCommandCreate } from "./commands/APPLICATION_COMMAND_CREATE.ts"; +import { handleApplicationCommandDelete } from "./commands/APPLICATION_COMMAND_DELETE.ts"; +import { handleApplicationCommandUpdate } from "./commands/APPLICATION_COMMAND_UPDATE.ts"; +import { handleGuildEmojisUpdate } from "./emojis/GUILD_EMOJIS_UPDATE.ts"; +import { handleGuildBanAdd } from "./guilds/GUILD_BAN_ADD.ts"; +import { handleGuildBanRemove } from "./guilds/GUILD_BAN_REMOVE.ts"; +import { handleGuildCreate } from "./guilds/GUILD_CREATE.ts"; +import { handleGuildDelete } from "./guilds/GUILD_DELETE.ts"; +import { handleGuildIntegrationsUpdate } from "./guilds/GUILD_INTEGRATIONS_UPDATE.ts"; +import { handleGuildUpdate } from "./guilds/GUILD_UPDATE.ts"; +import { handleIntegrationCreate } from "./integrations/INTEGRATION_CREATE.ts"; +import { handleIntegrationDelete } from "./integrations/INTEGRATION_DELETE.ts"; +import { handleIntegrationUpdate } from "./integrations/INTEGRATION_UPDATE.ts"; +import { handleInteractionCreate } from "./interactions/INTERACTION_CREATE.ts"; +import { handleInviteCreate } from "./invites/INVITE_CREATE.ts"; +import { handleGuildMembersChunk } from "./members/GUILD_MEMBERS_CHUNK.ts"; +import { handleGuildMemberAdd } from "./members/GUILD_MEMBER_ADD.ts"; +import { handleGuildMemberRemove } from "./members/GUILD_MEMBER_REMOVE.ts"; +import { handleGuildMemberUpdate } from "./members/GUILD_MEMBER_UPDATE.ts"; +import { handleMessageCreate } from "./messages/MESSAGE_CREATE.ts"; +import { handleMessageDelete } from "./messages/MESSAGE_DELETE.ts"; +import { handleMessageDeleteBulk } from "./messages/MESSAGE_DELETE_BULK.ts"; +import { handleMessageReactionAdd } from "./messages/MESSAGE_REACTION_ADD.ts"; +import { handleMessageReactionRemove } from "./messages/MESSAGE_REACTION_REMOVE.ts"; +import { handleMessageReactionRemoveAll } from "./messages/MESSAGE_REACTION_REMOVE_ALL.ts"; +import { handleMessageReactionRemoveEmoji } from "./messages/MESSAGE_REACTION_REMOVE_EMOJI.ts"; +import { handleMessageUpdate } from "./messages/MESSAGE_UPDATE.ts"; +import { handlePresenceUpdate } from "./presence/PRESENCE_UPDATE.ts"; +import { handleTypingStart } from "./presence/TYPING_START.ts"; +import { handleUserUpdate } from "./presence/USER_UPDATE.ts"; +import { handleReady } from "./READY.ts"; +import { handleGuildRoleCreate } from "./roles/GUILD_ROLE_CREATE.ts"; +import { handleGuildRoleDelete } from "./roles/GUILD_ROLE_DELETE.ts"; +import { handleGuildRoleUpdate } from "./roles/GUILD_ROLE_UPDATE.ts"; +import { handleVoiceServerUpdate } from "./voice/VOICE_SERVER_UPDATE.ts"; +import { handleVoiceStateUpdate } from "./voice/VOICE_STATE_UPDATE.ts"; +import { handleWebhooksUpdate } from "./webhooks/WEBHOOKS_UPDATE.ts"; + +export { + handleApplicationCommandCreate, + handleApplicationCommandDelete, + handleApplicationCommandUpdate, + handleChannelCreate, + handleChannelDelete, + handleChannelPinsUpdate, + handleChannelUpdate, + handleGuildBanAdd, + handleGuildBanRemove, + handleGuildCreate, + handleGuildDelete, + handleGuildEmojisUpdate, + handleGuildIntegrationsUpdate, + handleGuildMemberAdd, + handleGuildMemberRemove, + handleGuildMembersChunk, + handleGuildMemberUpdate, + handleGuildRoleCreate, + handleGuildRoleDelete, + handleGuildRoleUpdate, + handleGuildUpdate, + handleIntegrationCreate, + handleIntegrationDelete, + handleIntegrationUpdate, + handleInteractionCreate, + handleInviteCreate, + handleMessageCreate, + handleMessageDelete, + handleMessageDeleteBulk, + handleMessageReactionAdd, + handleMessageReactionRemove, + handleMessageReactionRemoveAll, + handleMessageReactionRemoveEmoji, + handleMessageUpdate, + handlePresenceUpdate, + handleReady, + handleTypingStart, + handleUserUpdate, + handleVoiceServerUpdate, + handleVoiceStateUpdate, + handleWebhooksUpdate, +}; + +export let handlers = { + READY: handleReady, + // channels + CHANNEL_CREATE: handleChannelCreate, + CHANNEL_DELETE: handleChannelDelete, + CHANNEL_PINS_UPDATE: handleChannelPinsUpdate, + CHANNEL_UPDATE: handleChannelUpdate, + // commands + APPLICATION_COMMAND_CREATE: handleApplicationCommandCreate, + APPLICATION_COMMAND_DELETE: handleApplicationCommandDelete, + APPLICATION_COMMAND_UPDATE: handleApplicationCommandUpdate, + // guilds + GUILD_BAN_ADD: handleGuildBanAdd, + GUILD_BAN_REMOVE: handleGuildBanRemove, + GUILD_CREATE: handleGuildCreate, + GUILD_DELETE: handleGuildDelete, + GUILD_EMOJIS_UPDATE: handleGuildEmojisUpdate, + GUILD_INTEGRATIONS_UPDATE: handleGuildIntegrationsUpdate, + GUILD_MEMBER_ADD: handleGuildMemberAdd, + GUILD_MEMBER_REMOVE: handleGuildMemberRemove, + GUILD_MEMBER_UPDATE: handleGuildMemberUpdate, + GUILD_MEMBERS_CHUNK: handleGuildMembersChunk, + GUILD_ROLE_CREATE: handleGuildRoleCreate, + GUILD_ROLE_DELETE: handleGuildRoleDelete, + GUILD_ROLE_UPDATE: handleGuildRoleUpdate, + GUILD_UPDATE: handleGuildUpdate, + // interactions + INTERACTION_CREATE: handleInteractionCreate, + // invites + INVITE_CREATE: handleInviteCreate, + INVITE_DELETE: handleInviteCreate, + // messages + MESSAGE_CREATE: handleMessageCreate, + MESSAGE_DELETE_BULK: handleMessageDeleteBulk, + MESSAGE_DELETE: handleMessageDelete, + MESSAGE_REACTION_ADD: handleMessageReactionAdd, + MESSAGE_REACTION_REMOVE_ALL: handleMessageReactionRemoveAll, + MESSAGE_REACTION_REMOVE_EMOJI: handleMessageReactionRemoveEmoji, + MESSAGE_REACTION_REMOVE: handleMessageReactionRemove, + MESSAGE_UPDATE: handleMessageUpdate, + // presence + PRESENCE_UPDATE: handlePresenceUpdate, + TYPING_START: handleTypingStart, + USER_UPDATE: handleUserUpdate, + // voice + VOICE_SERVER_UPDATE: handleVoiceServerUpdate, + VOICE_STATE_UPDATE: handleVoiceStateUpdate, + // webhooks + WEBHOOKS_UPDATE: handleWebhooksUpdate, + // integrations + INTEGRATION_CREATE: handleIntegrationCreate, + INTEGRATION_UPDATE: handleIntegrationUpdate, + INTEGRATION_DELETE: handleIntegrationDelete, +}; + +export type Handlers = typeof handlers; + +export function updateHandlers(newHandlers: Handlers) { + handlers = { + ...handlers, + ...newHandlers, + }; +} diff --git a/src/handlers/presence/PRESENCE_UPDATE.ts b/src/handlers/presence/PRESENCE_UPDATE.ts new file mode 100644 index 000000000..4e202e6ec --- /dev/null +++ b/src/handlers/presence/PRESENCE_UPDATE.ts @@ -0,0 +1,11 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, PresenceUpdatePayload } from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handlePresenceUpdate(data: DiscordPayload) { + const payload = data.d as PresenceUpdatePayload; + const oldPresence = await cacheHandlers.get("presences", payload.user.id); + await cacheHandlers.set("presences", payload.user.id, payload); + + eventHandlers.presenceUpdate?.(payload, oldPresence); +} diff --git a/src/handlers/presence/TYPING_START.ts b/src/handlers/presence/TYPING_START.ts new file mode 100644 index 000000000..308c88439 --- /dev/null +++ b/src/handlers/presence/TYPING_START.ts @@ -0,0 +1,6 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, TypingStartPayload } from "../../types/mod.ts"; + +export function handleTypingStart(data: DiscordPayload) { + eventHandlers.typingStart?.(data.d as TypingStartPayload); +} diff --git a/src/handlers/presence/USER_UPDATE.ts b/src/handlers/presence/USER_UPDATE.ts new file mode 100644 index 000000000..aade4ffcb --- /dev/null +++ b/src/handlers/presence/USER_UPDATE.ts @@ -0,0 +1,19 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, UserPayload } from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleUserUpdate(data: DiscordPayload) { + const userData = data.d as UserPayload; + + const member = await cacheHandlers.get("members", userData.id); + if (!member) return; + + Object.entries(userData).forEach(([key, value]) => { + // @ts-ignore index signatures + if (member[key] !== value) return member[key] = value; + }); + + await cacheHandlers.set("members", userData.id, member); + + eventHandlers.botUpdate?.(userData); +} diff --git a/src/handlers/roles/GUILD_ROLE_CREATE.ts b/src/handlers/roles/GUILD_ROLE_CREATE.ts new file mode 100644 index 000000000..121ca807a --- /dev/null +++ b/src/handlers/roles/GUILD_ROLE_CREATE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildRolePayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildRoleCreate(data: DiscordPayload) { + const payload = data.d as GuildRolePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const role = await structures.createRoleStruct(payload.role); + guild.roles = guild.roles.set(payload.role.id, role); + await cacheHandlers.set("guilds", payload.guild_id, guild); + + eventHandlers.roleCreate?.(guild, role); +} diff --git a/src/handlers/roles/GUILD_ROLE_DELETE.ts b/src/handlers/roles/GUILD_ROLE_DELETE.ts new file mode 100644 index 000000000..907abb0b2 --- /dev/null +++ b/src/handlers/roles/GUILD_ROLE_DELETE.ts @@ -0,0 +1,28 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildRoleDeletePayload } from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildRoleDelete(data: DiscordPayload) { + const payload = data.d as GuildRoleDeletePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const cachedRole = guild.roles.get(payload.role_id)!; + guild.roles.delete(payload.role_id); + + if (cachedRole) eventHandlers.roleDelete?.(guild, cachedRole); + + // For bots without GUILD_MEMBERS member.roles is never updated breaking permissions checking. + cacheHandlers.forEach("members", (member) => { + // Not in the relevant guild so just skip. + if (!member.guilds.has(guild.id)) return; + + member.guilds.forEach((g) => { + // Member does not have this role + if (!g.roles.includes(payload.role_id)) return; + // Remove this role from the members cache + g.roles = g.roles.filter((id) => id !== payload.role_id); + cacheHandlers.set("members", member.id, member); + }); + }); +} diff --git a/src/handlers/roles/GUILD_ROLE_UPDATE.ts b/src/handlers/roles/GUILD_ROLE_UPDATE.ts new file mode 100644 index 000000000..368c5ccb2 --- /dev/null +++ b/src/handlers/roles/GUILD_ROLE_UPDATE.ts @@ -0,0 +1,19 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, GuildRolePayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleGuildRoleUpdate(data: DiscordPayload) { + const payload = data.d as GuildRolePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const cachedRole = guild.roles.get(payload.role.id); + if (!cachedRole) return; + + const role = await structures.createRoleStruct(payload.role); + guild.roles.set(payload.role.id, role); + await cacheHandlers.set("guilds", guild.id, guild); + + eventHandlers.roleUpdate?.(guild, role, cachedRole); +} diff --git a/src/handlers/voice/VOICE_SERVER_UPDATE.ts b/src/handlers/voice/VOICE_SERVER_UPDATE.ts new file mode 100644 index 000000000..5da4ee508 --- /dev/null +++ b/src/handlers/voice/VOICE_SERVER_UPDATE.ts @@ -0,0 +1,15 @@ +import { eventHandlers } from "../../bot.ts"; +import { + DiscordPayload, + DiscordVoiceServerUpdateEvent, +} from "../../types/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleVoiceServerUpdate(data: DiscordPayload) { + const payload = data.d as DiscordVoiceServerUpdateEvent; + + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + eventHandlers.voiceServerUpdate?.(payload.token, guild, payload.endpoint); +} diff --git a/src/handlers/voice/VOICE_STATE_UPDATE.ts b/src/handlers/voice/VOICE_STATE_UPDATE.ts new file mode 100644 index 000000000..277285ee4 --- /dev/null +++ b/src/handlers/voice/VOICE_STATE_UPDATE.ts @@ -0,0 +1,54 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, VoiceStateUpdatePayload } from "../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../cache.ts"; + +export async function handleVoiceStateUpdate(data: DiscordPayload) { + const payload = data.d as VoiceStateUpdatePayload; + if (!payload.guild_id) return; + + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const member = payload.member + ? await structures.createMemberStruct(payload.member, guild.id) + : await cacheHandlers.get("members", payload.user_id); + if (!member) return; + + // No cached state before so lets make one for em + const cachedState = guild.voiceStates.get(payload.user_id); + + guild.voiceStates.set(payload.user_id, { + ...payload, + guildID: payload.guild_id, + channelID: payload.channel_id || "", + userID: payload.user_id, + sessionID: payload.session_id, + selfDeaf: payload.self_deaf, + selfMute: payload.self_mute, + selfStream: payload.self_stream || false, + }); + + await cacheHandlers.set("guilds", payload.guild_id, guild); + + if (cachedState?.channelID !== payload.channel_id) { + // Either joined or moved channels + if (payload.channel_id) { + if (cachedState?.channelID) { // Was in a channel before + eventHandlers.voiceChannelSwitch?.( + member, + payload.channel_id, + cachedState.channelID, + ); + } else { // Was not in a channel before so user just joined + eventHandlers.voiceChannelJoin?.(member, payload.channel_id); + } + } // Left the channel + else if (cachedState?.channelID) { + guild.voiceStates.delete(payload.user_id); + eventHandlers.voiceChannelLeave?.(member, cachedState.channelID); + } + } + + eventHandlers.voiceStateUpdate?.(member, payload); +} diff --git a/src/handlers/webhooks/WEBHOOKS_UPDATE.ts b/src/handlers/webhooks/WEBHOOKS_UPDATE.ts new file mode 100644 index 000000000..987683b9b --- /dev/null +++ b/src/handlers/webhooks/WEBHOOKS_UPDATE.ts @@ -0,0 +1,10 @@ +import { eventHandlers } from "../../bot.ts"; +import { DiscordPayload, WebhookUpdatePayload } from "../../types/mod.ts"; + +export function handleWebhooksUpdate(data: DiscordPayload) { + const options = data.d as WebhookUpdatePayload; + eventHandlers.webhooksUpdate?.( + options.channel_id, + options.guild_id, + ); +} diff --git a/src/helpers/channels/category_children_ids.ts b/src/helpers/channels/category_children_ids.ts new file mode 100644 index 000000000..36344a278 --- /dev/null +++ b/src/helpers/channels/category_children_ids.ts @@ -0,0 +1,9 @@ +import { cacheHandlers } from "../../cache.ts"; + +/** Gets an array of all the channels ids that are the children of this category. */ +export function categoryChildrenIDs(guildID: string, id: string) { + return cacheHandlers.filter( + "channels", + (channel) => channel.parentID === id && channel.guildID === guildID, + ); +} diff --git a/src/helpers/channels/channel_overwrite_has_permission.ts b/src/helpers/channels/channel_overwrite_has_permission.ts new file mode 100644 index 000000000..6eefaa998 --- /dev/null +++ b/src/helpers/channels/channel_overwrite_has_permission.ts @@ -0,0 +1,22 @@ +import { Permission, Permissions, RawOverwrite } from "../../types/mod.ts"; + +/** Checks if a channel overwrite for a user id or a role id has permission in this channel */ +export function channelOverwriteHasPermission( + guildID: string, + id: string, + overwrites: RawOverwrite[], + permissions: Permission[], +) { + const overwrite = overwrites.find((perm) => perm.id === id) || + overwrites.find((perm) => perm.id === guildID); + + return permissions.every((perm) => { + if (overwrite) { + const allowBits = overwrite.allow; + const denyBits = overwrite.deny; + if (BigInt(denyBits) & BigInt(Permissions[perm])) return false; + if (BigInt(allowBits) & BigInt(Permissions[perm])) return true; + } + return false; + }); +} diff --git a/src/helpers/channels/create_guild_channel.ts b/src/helpers/channels/create_guild_channel.ts new file mode 100644 index 000000000..ea5643921 --- /dev/null +++ b/src/helpers/channels/create_guild_channel.ts @@ -0,0 +1,50 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { + ChannelCreateOptions, + ChannelCreatePayload, + ChannelTypes, + Permission, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + calculateBits, + requireBotGuildPermissions, +} from "../../util/permissions.ts"; + +/** Create a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */ +export async function createGuildChannel( + guildID: string, + name: string, + options?: ChannelCreateOptions, +) { + const requiredPerms: Set = new Set(["MANAGE_CHANNELS"]); + + options?.permissionOverwrites?.forEach((overwrite) => { + overwrite.allow.forEach(requiredPerms.add, requiredPerms); + overwrite.deny.forEach(requiredPerms.add, requiredPerms); + }); + + await requireBotGuildPermissions(guildID, [...requiredPerms]); + + const result = (await RequestManager.post( + endpoints.GUILD_CHANNELS(guildID), + { + ...options, + name, + permission_overwrites: options?.permissionOverwrites?.map((perm) => ({ + ...perm, + + allow: calculateBits(perm.allow), + deny: calculateBits(perm.deny), + })), + type: options?.type || ChannelTypes.GUILD_TEXT, + }, + )) as ChannelCreatePayload; + + const channelStruct = await structures.createChannelStruct(result); + await cacheHandlers.set("channels", channelStruct.id, channelStruct); + + return channelStruct; +} diff --git a/src/helpers/channels/delete_channel.ts b/src/helpers/channels/delete_channel.ts new file mode 100644 index 000000000..967a0a46a --- /dev/null +++ b/src/helpers/channels/delete_channel.ts @@ -0,0 +1,32 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Delete a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */ +export async function deleteChannel( + guildID: string, + channelID: string, + reason?: string, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_CHANNELS"]); + + const guild = await cacheHandlers.get("guilds", guildID); + if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); + + if (guild?.rulesChannelID === channelID) { + throw new Error(Errors.RULES_CHANNEL_CANNOT_BE_DELETED); + } + + if (guild?.publicUpdatesChannelID === channelID) { + throw new Error(Errors.UPDATES_CHANNEL_CANNOT_BE_DELETED); + } + + const result = await RequestManager.delete( + endpoints.CHANNEL_BASE(channelID), + { reason }, + ); + + return result; +} diff --git a/src/helpers/channels/delete_channel_overwrite.ts b/src/helpers/channels/delete_channel_overwrite.ts new file mode 100644 index 000000000..39e7ebee1 --- /dev/null +++ b/src/helpers/channels/delete_channel_overwrite.ts @@ -0,0 +1,18 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Delete the channel permission overwrites for a user or role in this channel. Requires `MANAGE_ROLES` permission. */ +export async function deleteChannelOverwrite( + guildID: string, + channelID: string, + overwriteID: string, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); + + const result = await RequestManager.delete( + endpoints.CHANNEL_OVERWRITE(channelID, overwriteID), + ); + + return result; +} diff --git a/src/helpers/channels/edit_channel.ts b/src/helpers/channels/edit_channel.ts new file mode 100644 index 000000000..a8d83b341 --- /dev/null +++ b/src/helpers/channels/edit_channel.ts @@ -0,0 +1,109 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { ChannelEditOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + calculateBits, + requireBotChannelPermissions, +} from "../../util/permissions.ts"; + +/** Update a channel's settings. Requires the `MANAGE_CHANNELS` permission for the guild. */ +export async function editChannel( + channelID: string, + options: ChannelEditOptions, + reason?: string, +) { + await requireBotChannelPermissions(channelID, ["MANAGE_CHANNELS"]); + + if (options.name || options.topic) { + const request = editChannelNameTopicQueue.get(channelID); + if (!request) { + // If this hasnt been done before simply add 1 for it + editChannelNameTopicQueue.set(channelID, { + channelID: channelID, + amount: 1, + // 10 minutes from now + timestamp: Date.now() + 600000, + items: [], + }); + } else if (request.amount === 1) { + // Start queuing future requests to this channel + request.amount = 2; + request.timestamp = Date.now() + 600000; + } else { + // 2 have already been used add to queue + request.items.push({ channelID, options }); + if (editChannelProcessing) return; + editChannelProcessing = true; + processEditChannelQueue(); + return; + } + } + + const payload = { + ...options, + // deno-lint-ignore camelcase + rate_limit_per_user: options.rateLimitPerUser, + // deno-lint-ignore camelcase + parent_id: options.parentID, + // deno-lint-ignore camelcase + user_limit: options.userLimit, + // deno-lint-ignore camelcase + permission_overwrites: options.overwrites?.map((overwrite) => { + return { + ...overwrite, + allow: calculateBits(overwrite.allow), + deny: calculateBits(overwrite.deny), + }; + }), + }; + + const result = await RequestManager.patch(endpoints.CHANNEL_BASE(channelID), { + ...payload, + reason, + }); + + return result; +} + +interface EditChannelRequest { + amount: number; + timestamp: number; + channelID: string; + items: { + channelID: string; + options: ChannelEditOptions; + }[]; +} + +const editChannelNameTopicQueue = new Map(); +let editChannelProcessing = false; + +function processEditChannelQueue() { + if (!editChannelProcessing) return; + + const now = Date.now(); + editChannelNameTopicQueue.forEach((request) => { + if (now > request.timestamp) return; + // 10 minutes have passed so we can reset this channel again + if (!request.items.length) { + return editChannelNameTopicQueue.delete(request.channelID); + } + request.amount = 0; + // There are items to process for this request + const details = request.items.shift(); + + if (!details) return; + + editChannel(details.channelID, details.options); + const secondDetails = request.items.shift(); + if (!secondDetails) return; + + return editChannel(secondDetails.channelID, secondDetails.options); + }); + + if (editChannelNameTopicQueue.size) { + setTimeout(() => processEditChannelQueue(), 600000); + } else { + editChannelProcessing = false; + } +} diff --git a/src/helpers/channels/edit_channel_overwrite.ts b/src/helpers/channels/edit_channel_overwrite.ts new file mode 100644 index 000000000..ebda004a3 --- /dev/null +++ b/src/helpers/channels/edit_channel_overwrite.ts @@ -0,0 +1,28 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { Overwrite } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + calculateBits, + requireBotGuildPermissions, +} from "../../util/permissions.ts"; + +/** Edit the channel permission overwrites for a user or role in this channel. Requires `MANAGE_ROLES` permission. */ +export async function editChannelOverwrite( + guildID: string, + channelID: string, + overwriteID: string, + options: Omit, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); + + const result = await RequestManager.put( + endpoints.CHANNEL_OVERWRITE(channelID, overwriteID), + { + allow: calculateBits(options.allow), + deny: calculateBits(options.deny), + type: options.type, + }, + ); + + return result; +} diff --git a/src/helpers/channels/follow_channel.ts b/src/helpers/channels/follow_channel.ts new file mode 100644 index 000000000..621d79058 --- /dev/null +++ b/src/helpers/channels/follow_channel.ts @@ -0,0 +1,21 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { FollowedChannelPayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Follow a News Channel to send messages to a target channel. Requires the `MANAGE_WEBHOOKS` permission in the target channel. Returns the webhook id. */ +export async function followChannel( + sourceChannelID: string, + targetChannelID: string, +) { + await requireBotChannelPermissions(targetChannelID, ["MANAGE_WEBHOOKS"]); + + const data = (await RequestManager.post( + endpoints.CHANNEL_FOLLOW(sourceChannelID), + { + webhook_channel_id: targetChannelID, + }, + )) as FollowedChannelPayload; + + return data.webhook_id; +} diff --git a/src/helpers/channels/get_channel.ts b/src/helpers/channels/get_channel.ts new file mode 100644 index 000000000..ee9922d14 --- /dev/null +++ b/src/helpers/channels/get_channel.ts @@ -0,0 +1,25 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { ChannelCreatePayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Fetches a single channel object from the api. + * + * ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your channels will be cached in your guild.** + */ +export async function getChannel(channelID: string, addToCache = true) { + const result = (await RequestManager.get( + endpoints.CHANNEL_BASE(channelID), + )) as ChannelCreatePayload; + + const channelStruct = await structures.createChannelStruct( + result, + result.guild_id, + ); + if (addToCache) { + await cacheHandlers.set("channels", channelStruct.id, channelStruct); + } + + return channelStruct; +} diff --git a/src/helpers/channels/get_channel_webhooks.ts b/src/helpers/channels/get_channel_webhooks.ts new file mode 100644 index 000000000..5d9787eba --- /dev/null +++ b/src/helpers/channels/get_channel_webhooks.ts @@ -0,0 +1,15 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { WebhookPayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Gets the webhooks for this channel. Requires MANAGE_WEBHOOKS */ +export async function getChannelWebhooks(channelID: string) { + await requireBotChannelPermissions(channelID, ["MANAGE_WEBHOOKS"]); + + const result = await RequestManager.get( + endpoints.CHANNEL_WEBHOOKS(channelID), + ); + + return result as WebhookPayload[]; +} diff --git a/src/helpers/channels/get_channels.ts b/src/helpers/channels/get_channels.ts new file mode 100644 index 000000000..0bf1ee856 --- /dev/null +++ b/src/helpers/channels/get_channels.ts @@ -0,0 +1,24 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { ChannelCreatePayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns a list of guild channel objects. + * + * ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your channels will be cached in your guild.** + */ +export async function getChannels(guildID: string, addToCache = true) { + const result = (await RequestManager.get( + endpoints.GUILD_CHANNELS(guildID), + ) as ChannelCreatePayload[]); + + return Promise.all(result.map(async (res) => { + const channelStruct = await structures.createChannelStruct(res, guildID); + if (addToCache) { + await cacheHandlers.set("channels", channelStruct.id, channelStruct); + } + + return channelStruct; + })); +} diff --git a/src/helpers/channels/get_pins.ts b/src/helpers/channels/get_pins.ts new file mode 100644 index 000000000..6a9e0b93a --- /dev/null +++ b/src/helpers/channels/get_pins.ts @@ -0,0 +1,15 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { MessageCreateOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Get pinned messages in this channel. */ +export async function getPins(channelID: string) { + const result = (await RequestManager.get( + endpoints.CHANNEL_PINS(channelID), + )) as MessageCreateOptions[]; + + return Promise.all( + result.map((res) => structures.createMessageStruct(res)), + ); +} diff --git a/src/helpers/channels/is_channel_synced.ts b/src/helpers/channels/is_channel_synced.ts new file mode 100644 index 000000000..abeebe520 --- /dev/null +++ b/src/helpers/channels/is_channel_synced.ts @@ -0,0 +1,20 @@ +import { cacheHandlers } from "../../cache.ts"; + +/** Checks whether a channel is synchronized with its parent/category channel or not. */ +export async function isChannelSynced(channelID: string) { + const channel = await cacheHandlers.get("channels", channelID); + if (!channel?.parentID) return false; + + const parentChannel = await cacheHandlers.get("channels", channel.parentID); + if (!parentChannel) return false; + + return channel.permissionOverwrites?.every((overwrite) => { + const permission = parentChannel.permissionOverwrites?.find( + (ow) => ow.id === overwrite.id, + ); + if (!permission) return false; + return !( + overwrite.allow !== permission.allow || overwrite.deny !== permission.deny + ); + }); +} diff --git a/src/helpers/channels/start_typing.ts b/src/helpers/channels/start_typing.ts new file mode 100644 index 000000000..ff9f2502b --- /dev/null +++ b/src/helpers/channels/start_typing.ts @@ -0,0 +1,40 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { ChannelTypes, Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { botHasChannelPermissions } from "../../util/permissions.ts"; + +/** + * Trigger a typing indicator for the specified channel. Generally bots should **NOT** implement this route. + * However, if a bot is responding to a command and expects the computation to take a few seconds, + * this endpoint may be called to let the user know that the bot is processing their message. + */ +export async function startTyping(channelID: string) { + const channel = await cacheHandlers.get("channels", channelID); + // If the channel is cached, we can do extra checks/safety + if (channel) { + if ( + ![ + ChannelTypes.DM, + ChannelTypes.GUILD_NEWS, + ChannelTypes.GUILD_TEXT, + ].includes(channel.type) + ) { + throw new Error(Errors.CHANNEL_NOT_TEXT_BASED); + } + + const hasSendMessagesPerm = await botHasChannelPermissions( + channelID, + ["SEND_MESSAGES"], + ); + if ( + !hasSendMessagesPerm + ) { + throw new Error(Errors.MISSING_SEND_MESSAGES); + } + } + + const result = await RequestManager.post(endpoints.CHANNEL_TYPING(channelID)); + + return result; +} diff --git a/src/helpers/channels/swap_channels.ts b/src/helpers/channels/swap_channels.ts new file mode 100644 index 000000000..49adc9d84 --- /dev/null +++ b/src/helpers/channels/swap_channels.ts @@ -0,0 +1,20 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { PositionSwap } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Modify the positions of channels on the guild. Requires MANAGE_CHANNELS permisison. */ +export async function swapChannels( + guildID: string, + channelPositions: PositionSwap[], +) { + if (channelPositions.length < 2) { + throw "You must provide at least two channels to be swapped."; + } + + const result = await RequestManager.patch( + endpoints.GUILD_CHANNELS(guildID), + channelPositions, + ); + + return result; +} diff --git a/src/helpers/commands/create_slash_command.ts b/src/helpers/commands/create_slash_command.ts new file mode 100644 index 000000000..42fd65197 --- /dev/null +++ b/src/helpers/commands/create_slash_command.ts @@ -0,0 +1,31 @@ +import { applicationID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { CreateSlashCommandOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { validateSlashCommands } from "../../util/utils.ts"; + +/** + * There are two kinds of Slash Commands: global commands and guild commands. Global commands are available for every guild that adds your app; guild commands are specific to the guild you specify when making them. Command names are unique per application within each scope (global and guild). That means: + * + * - Your app **cannot** have two global commands with the same name + * - Your app **cannot** have two guild commands within the same name **on the same guild** + * - Your app **can** have a global and guild command with the same name + * - Multiple apps **can** have commands with the same names + * + * Global commands are cached for **1 hour**. That means that new global commands will fan out slowly across all guilds, and will be guaranteed to be updated in an hour. + * Guild commands update **instantly**. We recommend you use guild commands for quick testing, and global commands when they're ready for public use. + */ +export async function createSlashCommand(options: CreateSlashCommandOptions) { + validateSlashCommands([options], true); + + const result = await RequestManager.post( + options.guildID + ? endpoints.COMMANDS_GUILD(applicationID, options.guildID) + : endpoints.COMMANDS(applicationID), + { + ...options, + }, + ); + + return result; +} diff --git a/src/helpers/commands/delete_slash_command.ts b/src/helpers/commands/delete_slash_command.ts new file mode 100644 index 000000000..2dd7fba57 --- /dev/null +++ b/src/helpers/commands/delete_slash_command.ts @@ -0,0 +1,13 @@ +import { applicationID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Deletes a slash command. */ +export function deleteSlashCommand(id: string, guildID?: string) { + if (!guildID) { + return RequestManager.delete(endpoints.COMMANDS_ID(applicationID, id)); + } + return RequestManager.delete( + endpoints.COMMANDS_GUILD_ID(applicationID, guildID, id), + ); +} diff --git a/src/helpers/commands/delete_slash_response.ts b/src/helpers/commands/delete_slash_response.ts new file mode 100644 index 000000000..0118e186f --- /dev/null +++ b/src/helpers/commands/delete_slash_response.ts @@ -0,0 +1,18 @@ +import { applicationID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** To delete your response to a slash command. If a message id is not provided, it will default to deleting the original response. */ +export async function deleteSlashResponse(token: string, messageID?: string) { + const result = await RequestManager.delete( + messageID + ? endpoints.INTERACTION_ID_TOKEN_MESSAGEID( + applicationID, + token, + messageID, + ) + : endpoints.INTERACTION_ORIGINAL_ID_TOKEN(applicationID, token), + ); + + return result; +} diff --git a/src/helpers/commands/edit_slash_response.ts b/src/helpers/commands/edit_slash_response.ts new file mode 100644 index 000000000..2165ca266 --- /dev/null +++ b/src/helpers/commands/edit_slash_response.ts @@ -0,0 +1,70 @@ +import { applicationID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { + EditSlashResponseOptions, + Errors, + MessageCreateOptions, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** To edit your response to a slash command. If a messageID is not provided it will default to editing the original response. */ +export async function editSlashResponse( + token: string, + options: EditSlashResponseOptions, +) { + if (options.content && options.content.length > 2000) { + throw Error(Errors.MESSAGE_MAX_LENGTH); + } + + if (options.embeds && options.embeds.length > 10) { + options.embeds.splice(10); + } + + if (options.allowed_mentions) { + if (options.allowed_mentions.users?.length) { + if (options.allowed_mentions.parse.includes("users")) { + options.allowed_mentions.parse = options.allowed_mentions.parse.filter( + (p) => p !== "users", + ); + } + + if (options.allowed_mentions.users.length > 100) { + options.allowed_mentions.users = options.allowed_mentions.users.slice( + 0, + 100, + ); + } + } + + if (options.allowed_mentions.roles?.length) { + if (options.allowed_mentions.parse.includes("roles")) { + options.allowed_mentions.parse = options.allowed_mentions.parse.filter( + (p) => p !== "roles", + ); + } + + if (options.allowed_mentions.roles.length > 100) { + options.allowed_mentions.roles = options.allowed_mentions.roles.slice( + 0, + 100, + ); + } + } + } + + const result = await RequestManager.patch( + options.messageID + ? endpoints.WEBHOOK_MESSAGE(applicationID, token, options.messageID) + : endpoints.INTERACTION_ORIGINAL_ID_TOKEN(applicationID, token), + options, + ); + + // If the original message was edited, this will not return a message + if (!options.messageID) return result; + + const message = await structures.createMessageStruct( + result as MessageCreateOptions, + ); + return message; +} diff --git a/src/helpers/commands/execute_slash_command.ts b/src/helpers/commands/execute_slash_command.ts new file mode 100644 index 000000000..42a652214 --- /dev/null +++ b/src/helpers/commands/execute_slash_command.ts @@ -0,0 +1,48 @@ +import { applicationID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { SlashCommandResponseOptions } from "../../types/mod.ts"; +import { cache } from "../../util/cache.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** + * Send a response to a users slash command. The command data will have the id and token necessary to respond. + * Interaction `tokens` are valid for **15 minutes** and can be used to send followup messages. + * + * NOTE: By default we will suppress mentions. To enable mentions, just pass any mentions object. + */ +export async function executeSlashCommand( + id: string, + token: string, + options: SlashCommandResponseOptions, +) { + // If its already been executed, we need to send a followup response + if (cache.executedSlashCommands.has(token)) { + return RequestManager.post(endpoints.WEBHOOK(applicationID, token), { + ...options, + }); + } + + // Expire in 15 minutes + cache.executedSlashCommands.set(token, id); + setTimeout( + () => cache.executedSlashCommands.delete(token), + 900000, + ); + + // If the user wants this as a private message mark it ephemeral + if (options.private) { + options.data.flags = 64; + } + + // If no mentions are provided, force disable mentions + if (!options.data.allowed_mentions) { + options.data.allowed_mentions = { parse: [] }; + } + + const result = await RequestManager.post( + endpoints.INTERACTION_ID_TOKEN(id, token), + options, + ); + + return result; +} diff --git a/src/helpers/commands/get_slash_command.ts b/src/helpers/commands/get_slash_command.ts new file mode 100644 index 000000000..8cf41c003 --- /dev/null +++ b/src/helpers/commands/get_slash_command.ts @@ -0,0 +1,15 @@ +import { applicationID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { SlashCommand } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Fetchs the global command for the given ID. If a guildID is provided, the guild command will be fetched. */ +export async function getSlashCommand(commandID: string, guildID?: string) { + const result = await RequestManager.get( + guildID + ? endpoints.COMMANDS_GUILD_ID(applicationID, guildID, commandID) + : endpoints.COMMANDS_ID(applicationID, commandID), + ); + + return result as SlashCommand; +} diff --git a/src/helpers/commands/get_slash_commands.ts b/src/helpers/commands/get_slash_commands.ts new file mode 100644 index 000000000..1f256c148 --- /dev/null +++ b/src/helpers/commands/get_slash_commands.ts @@ -0,0 +1,16 @@ +import { applicationID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { SlashCommand } from "../../types/mod.ts"; +import { Collection } from "../../util/collection.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Fetch all of the global commands for your application. */ +export async function getSlashCommands(guildID?: string) { + const result = (await RequestManager.get( + guildID + ? endpoints.COMMANDS_GUILD(applicationID, guildID) + : endpoints.COMMANDS(applicationID), + )) as SlashCommand[]; + + return new Collection(result.map((command) => [command.name, command])); +} diff --git a/src/helpers/commands/upsert_slash_command.ts b/src/helpers/commands/upsert_slash_command.ts new file mode 100644 index 000000000..49bd9eed1 --- /dev/null +++ b/src/helpers/commands/upsert_slash_command.ts @@ -0,0 +1,25 @@ +import { applicationID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { UpsertSlashCommandOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { validateSlashCommands } from "../../util/utils.ts"; + +/** + * Edit an existing slash command. If this command did not exist, it will create it. + */ +export async function upsertSlashCommand( + commandID: string, + options: UpsertSlashCommandOptions, + guildID?: string, +) { + validateSlashCommands([options]); + + const result = await RequestManager.patch( + guildID + ? endpoints.COMMANDS_GUILD_ID(applicationID, guildID, commandID) + : endpoints.COMMANDS_ID(applicationID, commandID), + options, + ); + + return result; +} diff --git a/src/helpers/commands/upsert_slash_commands.ts b/src/helpers/commands/upsert_slash_commands.ts new file mode 100644 index 000000000..336220769 --- /dev/null +++ b/src/helpers/commands/upsert_slash_commands.ts @@ -0,0 +1,26 @@ +import { applicationID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { UpsertSlashCommandsOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { validateSlashCommands } from "../../util/utils.ts"; + +/** + * Bulk edit existing slash commands. If a command does not exist, it will create it. + * + * **NOTE:** Any slash commands that are not specified in this function will be **deleted**. If you don't provide the commandID and rename your command, the command gets a new ID. + */ +export async function upsertSlashCommands( + options: UpsertSlashCommandsOptions[], + guildID?: string, +) { + validateSlashCommands(options); + + const result = await RequestManager.put( + guildID + ? endpoints.COMMANDS_GUILD(applicationID, guildID) + : endpoints.COMMANDS(applicationID), + options, + ); + + return result; +} diff --git a/src/helpers/emojis/create_emoji.ts b/src/helpers/emojis/create_emoji.ts new file mode 100644 index 000000000..139fad0ad --- /dev/null +++ b/src/helpers/emojis/create_emoji.ts @@ -0,0 +1,27 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { CreateEmojisOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; +import { urlToBase64 } from "../../util/utils.ts"; + +/** Create an emoji in the server. Emojis and animated emojis have a maximum file size of 256kb. Attempting to upload an emoji larger than this limit will fail and return 400 Bad Request and an error message, but not a JSON status code. If a URL is provided to the image parameter, Discordeno will automatically convert it to a base64 string internally. */ +export async function createEmoji( + guildID: string, + name: string, + image: string, + options: CreateEmojisOptions, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_EMOJIS"]); + + if (image && !image.startsWith("data:image/")) { + image = await urlToBase64(image); + } + + const result = await RequestManager.post(endpoints.GUILD_EMOJIS(guildID), { + ...options, + name, + image, + }); + + return result; +} diff --git a/src/helpers/emojis/delete_emoji.ts b/src/helpers/emojis/delete_emoji.ts new file mode 100644 index 000000000..a9ef8c2d8 --- /dev/null +++ b/src/helpers/emojis/delete_emoji.ts @@ -0,0 +1,19 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Delete the given emoji. Requires the MANAGE_EMOJIS permission. Returns 204 No Content on success. */ +export async function deleteEmoji( + guildID: string, + id: string, + reason?: string, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_EMOJIS"]); + + const result = await RequestManager.delete( + endpoints.GUILD_EMOJI(guildID, id), + { reason }, + ); + + return result; +} diff --git a/src/helpers/emojis/edit_emoji.ts b/src/helpers/emojis/edit_emoji.ts new file mode 100644 index 000000000..13b201d12 --- /dev/null +++ b/src/helpers/emojis/edit_emoji.ts @@ -0,0 +1,23 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { EditEmojisOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Modify the given emoji. Requires the MANAGE_EMOJIS permission. */ +export async function editEmoji( + guildID: string, + id: string, + options: EditEmojisOptions, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_EMOJIS"]); + + const result = await RequestManager.patch( + endpoints.GUILD_EMOJI(guildID, id), + { + name: options.name, + roles: options.roles, + }, + ); + + return result; +} diff --git a/src/helpers/emojis/emoji_url.ts b/src/helpers/emojis/emoji_url.ts new file mode 100644 index 000000000..75360f838 --- /dev/null +++ b/src/helpers/emojis/emoji_url.ts @@ -0,0 +1,4 @@ +/** Creates a url to the emoji from the Discord CDN. */ +export function emojiURL(id: string, animated = false) { + return `https://cdn.discordapp.com/emojis/${id}.${animated ? "gif" : "png"}`; +} diff --git a/src/helpers/emojis/get_emoji.ts b/src/helpers/emojis/get_emoji.ts new file mode 100644 index 000000000..82aa275aa --- /dev/null +++ b/src/helpers/emojis/get_emoji.ts @@ -0,0 +1,32 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Emoji, Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** + * Returns an emoji for the given guild and emoji ID. + * + * ⚠️ **If you need this, you are probably doing something wrong. Always use cache.guilds.get()?.emojis + */ +export async function getEmoji( + guildID: string, + emojiID: string, + addToCache = true, +) { + const result = (await RequestManager.get( + endpoints.GUILD_EMOJI(guildID, emojiID), + )) as Emoji; + + if (addToCache) { + const guild = await cacheHandlers.get("guilds", guildID); + if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); + guild.emojis.set(result.id ?? result.name, result); + cacheHandlers.set( + "guilds", + guildID, + guild, + ); + } + + return result; +} diff --git a/src/helpers/emojis/get_emojis.ts b/src/helpers/emojis/get_emojis.ts new file mode 100644 index 000000000..772cbbcd9 --- /dev/null +++ b/src/helpers/emojis/get_emojis.ts @@ -0,0 +1,26 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Emoji, Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** + * Returns a list of emojis for the given guild. + * + * ⚠️ **If you need this, you are probably doing something wrong. Always use cache.guilds.get()?.emojis + */ +export async function getEmojis(guildID: string, addToCache = true) { + const result = (await RequestManager.get( + endpoints.GUILD_EMOJIS(guildID), + )) as Emoji[]; + + if (addToCache) { + const guild = await cacheHandlers.get("guilds", guildID); + if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); + + result.forEach((emoji) => guild.emojis.set(emoji.id ?? emoji.name, emoji)); + + cacheHandlers.set("guilds", guildID, guild); + } + + return result; +} diff --git a/src/helpers/guilds/create_guild.ts b/src/helpers/guilds/create_guild.ts new file mode 100644 index 000000000..7ca748d1f --- /dev/null +++ b/src/helpers/guilds/create_guild.ts @@ -0,0 +1,14 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { CreateGuildPayload, CreateServerOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. This endpoint can be used only by bots in less than 10 guilds. */ +export async function createGuild(options: CreateServerOptions) { + const guild = (await RequestManager.post( + endpoints.GUILDS, + options, + )) as CreateGuildPayload; + + return structures.createGuildStruct(guild, 0); +} diff --git a/src/helpers/guilds/delete_server.ts b/src/helpers/guilds/delete_server.ts new file mode 100644 index 000000000..dd66c9188 --- /dev/null +++ b/src/helpers/guilds/delete_server.ts @@ -0,0 +1,10 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Delete a guild permanently. User must be owner. Returns 204 No Content on success. Fires a Guild Delete Gateway event. + */ +export async function deleteServer(guildID: string) { + const result = await RequestManager.delete(endpoints.GUILDS_BASE(guildID)); + + return result; +} diff --git a/src/helpers/guilds/edit_guild.ts b/src/helpers/guilds/edit_guild.ts new file mode 100644 index 000000000..2cbc13fbe --- /dev/null +++ b/src/helpers/guilds/edit_guild.ts @@ -0,0 +1,29 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { GuildEditOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; +import { urlToBase64 } from "../../util/utils.ts"; + +/** Modify a guilds settings. Requires the MANAGE_GUILD permission. */ +export async function editGuild(guildID: string, options: GuildEditOptions) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + if (options.icon && !options.icon.startsWith("data:image/")) { + options.icon = await urlToBase64(options.icon); + } + + if (options.banner && !options.banner.startsWith("data:image/")) { + options.banner = await urlToBase64(options.banner); + } + + if (options.splash && !options.splash.startsWith("data:image/")) { + options.splash = await urlToBase64(options.splash); + } + + const result = await RequestManager.patch( + endpoints.GUILDS_BASE(guildID), + options, + ); + + return result; +} diff --git a/src/helpers/guilds/edit_widget.ts b/src/helpers/guilds/edit_widget.ts new file mode 100644 index 000000000..9f7a91605 --- /dev/null +++ b/src/helpers/guilds/edit_widget.ts @@ -0,0 +1,19 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Modify a guild widget object for the guild. Requires the MANAGE_GUILD permission. */ +export async function editWidget( + guildID: string, + enabled: boolean, + channelID?: string | null, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const result = await RequestManager.patch(endpoints.GUILD_WIDGET(guildID), { + enabled, + channel_id: channelID, + }); + + return result; +} diff --git a/src/helpers/guilds/get_audit_logs.ts b/src/helpers/guilds/get_audit_logs.ts new file mode 100644 index 000000000..b28004e31 --- /dev/null +++ b/src/helpers/guilds/get_audit_logs.ts @@ -0,0 +1,24 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { AuditLogs, GetAuditLogsOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */ +export async function getAuditLogs( + guildID: string, + options: GetAuditLogsOptions, +) { + await requireBotGuildPermissions(guildID, ["VIEW_AUDIT_LOG"]); + + const result = await RequestManager.get(endpoints.GUILD_AUDIT_LOGS(guildID), { + ...options, + action_type: options.action_type + ? AuditLogs[options.action_type] + : undefined, + limit: options.limit && options.limit >= 1 && options.limit <= 100 + ? options.limit + : 50, + }); + + return result; +} diff --git a/src/helpers/guilds/get_available_voice_regions.ts b/src/helpers/guilds/get_available_voice_regions.ts new file mode 100644 index 000000000..42d29c02e --- /dev/null +++ b/src/helpers/guilds/get_available_voice_regions.ts @@ -0,0 +1,9 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns an array of voice regions that can be used when creating servers. */ +export async function getAvailableVoiceRegions() { + const result = await RequestManager.get(endpoints.VOICE_REGIONS); + + return result; +} diff --git a/src/helpers/guilds/get_ban.ts b/src/helpers/guilds/get_ban.ts new file mode 100644 index 000000000..1b52f3671 --- /dev/null +++ b/src/helpers/guilds/get_ban.ts @@ -0,0 +1,15 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { BannedUser } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. */ +export async function getBan(guildID: string, memberID: string) { + await requireBotGuildPermissions(guildID, ["BAN_MEMBERS"]); + + const result = await RequestManager.get( + endpoints.GUILD_BAN(guildID, memberID), + ); + + return result as BannedUser; +} diff --git a/src/helpers/guilds/get_bans.ts b/src/helpers/guilds/get_bans.ts new file mode 100644 index 000000000..bacb913bd --- /dev/null +++ b/src/helpers/guilds/get_bans.ts @@ -0,0 +1,18 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { BannedUser } from "../../types/mod.ts"; +import { Collection } from "../../util/collection.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */ +export async function getBans(guildID: string) { + await requireBotGuildPermissions(guildID, ["BAN_MEMBERS"]); + + const results = (await RequestManager.get( + endpoints.GUILD_BANS(guildID), + )) as BannedUser[]; + + return new Collection( + results.map((res) => [res.user.id, res]), + ); +} diff --git a/src/helpers/guilds/get_guild.ts b/src/helpers/guilds/get_guild.ts new file mode 100644 index 000000000..c1c2c516c --- /dev/null +++ b/src/helpers/guilds/get_guild.ts @@ -0,0 +1,18 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { UpdateGuildPayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** + * ⚠️ **If you need this, you are probably doing something wrong. Always use cache.guilds.get() + * + * Advanced Devs: + * This function fetches a guild's data. This is not the same data as a GUILD_CREATE. + * So it does not cache the guild, you must do it manually. + * */ +export async function getGuild(guildID: string, counts = true) { + const result = await RequestManager.get(endpoints.GUILDS_BASE(guildID), { + with_counts: counts, + }); + + return result as UpdateGuildPayload; +} diff --git a/src/helpers/guilds/get_guild_preview.ts b/src/helpers/guilds/get_guild_preview.ts new file mode 100644 index 000000000..dda89c735 --- /dev/null +++ b/src/helpers/guilds/get_guild_preview.ts @@ -0,0 +1,9 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns the guild preview object for the given id. If the bot is not in the guild, then the guild must be Discoverable. */ +export async function getGuildPreview(guildID: string) { + const result = await RequestManager.get(endpoints.GUILD_PREVIEW(guildID)); + + return result; +} diff --git a/src/helpers/guilds/get_prune_count.ts b/src/helpers/guilds/get_prune_count.ts new file mode 100644 index 000000000..9fbe2d9d2 --- /dev/null +++ b/src/helpers/guilds/get_prune_count.ts @@ -0,0 +1,22 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors, PruneOptions, PrunePayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; +import { camelKeysToSnakeCase, urlToBase64 } from "../../util/utils.ts"; + +/** Check how many members would be removed from the server in a prune operation. Requires the KICK_MEMBERS permission */ +export async function getPruneCount(guildID: string, options?: PruneOptions) { + if (options?.days && options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); + if (options?.days && options.days > 30) { + throw new Error(Errors.PRUNE_MAX_DAYS); + } + + await requireBotGuildPermissions(guildID, ["KICK_MEMBERS"]); + + const result = await RequestManager.get( + endpoints.GUILD_PRUNE(guildID), + camelKeysToSnakeCase(options ?? {}), + ) as PrunePayload; + + return result.pruned; +} diff --git a/src/helpers/guilds/get_vainty_url.ts b/src/helpers/guilds/get_vainty_url.ts new file mode 100644 index 000000000..1b6e044cd --- /dev/null +++ b/src/helpers/guilds/get_vainty_url.ts @@ -0,0 +1,9 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns the code and uses of the vanity url for this server if it is enabled. Requires the MANAGE_GUILD permission. */ +export async function getVanityURL(guildID: string) { + const result = await RequestManager.get(endpoints.GUILD_VANITY_URL(guildID)); + + return result; +} diff --git a/src/helpers/guilds/get_voice_regions.ts b/src/helpers/guilds/get_voice_regions.ts new file mode 100644 index 000000000..008652087 --- /dev/null +++ b/src/helpers/guilds/get_voice_regions.ts @@ -0,0 +1,9 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns a list of voice region objects for the guild. Unlike the similar /voice route, this returns VIP servers when the guild is VIP-enabled. */ +export async function getVoiceRegions(guildID: string) { + const result = await RequestManager.get(endpoints.GUILD_REGIONS(guildID)); + + return result; +} diff --git a/src/helpers/guilds/get_widget.ts b/src/helpers/guilds/get_widget.ts new file mode 100644 index 000000000..4915059cb --- /dev/null +++ b/src/helpers/guilds/get_widget.ts @@ -0,0 +1,15 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns the widget for the guild. */ +export async function getWidget(guildID: string, options?: { force: boolean }) { + if (!options?.force) { + const guild = await cacheHandlers.get("guilds", guildID); + if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); + if (!guild?.widgetEnabled) throw new Error(Errors.GUILD_WIDGET_NOT_ENABLED); + } + + return RequestManager.get(`${endpoints.GUILD_WIDGET(guildID)}.json`); +} diff --git a/src/helpers/guilds/get_widget_image_url.ts b/src/helpers/guilds/get_widget_image_url.ts new file mode 100644 index 000000000..b2ad2d22e --- /dev/null +++ b/src/helpers/guilds/get_widget_image_url.ts @@ -0,0 +1,21 @@ +import { cacheHandlers } from "../../cache.ts"; +import { Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns the widget image URL for the guild. */ +export async function getWidgetImageURL( + guildID: string, + options?: { + style?: "shield" | "banner1" | "banner2" | "banner3" | "banner4"; + force?: boolean; + }, +) { + if (!options?.force) { + const guild = await cacheHandlers.get("guilds", guildID); + if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); + if (!guild.widgetEnabled) throw new Error(Errors.GUILD_WIDGET_NOT_ENABLED); + } + + return `${endpoints.GUILD_WIDGET(guildID)}.png?style=${options?.style ?? + "shield"}`; +} diff --git a/src/helpers/guilds/get_widget_settings.ts b/src/helpers/guilds/get_widget_settings.ts new file mode 100644 index 000000000..a4893eefb --- /dev/null +++ b/src/helpers/guilds/get_widget_settings.ts @@ -0,0 +1,12 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Returns the guild widget object. Requires the MANAGE_GUILD permission. */ +export async function getWidgetSettings(guildID: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const result = await RequestManager.get(endpoints.GUILD_WIDGET(guildID)); + + return result; +} diff --git a/src/helpers/guilds/guild_banner_url.ts b/src/helpers/guilds/guild_banner_url.ts new file mode 100644 index 000000000..2c6a50b2e --- /dev/null +++ b/src/helpers/guilds/guild_banner_url.ts @@ -0,0 +1,19 @@ +import { Guild } from "../../structures/mod.ts"; +import { ImageFormats, ImageSize } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { formatImageURL } from "../../util/utils.ts"; + +/** The full URL of the banner from Discords CDN. Undefined if no banner is set. */ +export function guildBannerURL( + guild: Guild, + size: ImageSize = 128, + format?: ImageFormats, +) { + return guild.banner + ? formatImageURL( + endpoints.GUILD_BANNER(guild.id, guild.banner), + size, + format, + ) + : undefined; +} diff --git a/src/helpers/guilds/guild_icon_url.ts b/src/helpers/guilds/guild_icon_url.ts new file mode 100644 index 000000000..b8602fcaa --- /dev/null +++ b/src/helpers/guilds/guild_icon_url.ts @@ -0,0 +1,15 @@ +import { Guild } from "../../structures/mod.ts"; +import { ImageFormats, ImageSize } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { formatImageURL } from "../../util/utils.ts"; + +/** The full URL of the icon from Discords CDN. Undefined when no icon is set. */ +export function guildIconURL( + guild: Guild, + size: ImageSize = 128, + format?: ImageFormats, +) { + return guild.icon + ? formatImageURL(endpoints.GUILD_ICON(guild.id, guild.icon), size, format) + : undefined; +} diff --git a/src/helpers/guilds/guild_splash_url.ts b/src/helpers/guilds/guild_splash_url.ts new file mode 100644 index 000000000..7383ef7a9 --- /dev/null +++ b/src/helpers/guilds/guild_splash_url.ts @@ -0,0 +1,19 @@ +import { Guild } from "../../structures/mod.ts"; +import { ImageFormats, ImageSize } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { formatImageURL } from "../../util/utils.ts"; + +/** The full URL of the splash from Discords CDN. Undefined if no splash is set. */ +export function guildSplashURL( + guild: Guild, + size: ImageSize = 128, + format?: ImageFormats, +) { + return guild.splash + ? formatImageURL( + endpoints.GUILD_SPLASH(guild.id, guild.splash), + size, + format, + ) + : undefined; +} diff --git a/src/helpers/guilds/leave_guild.ts b/src/helpers/guilds/leave_guild.ts new file mode 100644 index 000000000..78f1e3ed4 --- /dev/null +++ b/src/helpers/guilds/leave_guild.ts @@ -0,0 +1,9 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Leave a guild */ +export async function leaveGuild(guildID: string) { + const result = await RequestManager.delete(endpoints.GUILD_LEAVE(guildID)); + + return result; +} diff --git a/src/helpers/integrations/delete_integration.ts b/src/helpers/integrations/delete_integration.ts new file mode 100644 index 000000000..69f269a58 --- /dev/null +++ b/src/helpers/integrations/delete_integration.ts @@ -0,0 +1,14 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Delete the attached integration object for the guild with this id. Requires MANAGE_GUILD permission. */ +export async function deleteIntegration(guildID: string, id: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const result = await RequestManager.delete( + endpoints.GUILD_INTEGRATION(guildID, id), + ); + + return result; +} diff --git a/src/helpers/integrations/edit_integration.ts b/src/helpers/integrations/edit_integration.ts new file mode 100644 index 000000000..5fc158094 --- /dev/null +++ b/src/helpers/integrations/edit_integration.ts @@ -0,0 +1,20 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { EditIntegrationOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Modify the behavior and settings of an integration object for the guild. Requires the MANAGE_GUILD permission. */ +export async function editIntegration( + guildID: string, + id: string, + options: EditIntegrationOptions, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const result = await RequestManager.patch( + endpoints.GUILD_INTEGRATION(guildID, id), + options, + ); + + return result; +} diff --git a/src/helpers/integrations/get_integrations.ts b/src/helpers/integrations/get_integrations.ts new file mode 100644 index 000000000..508862d8d --- /dev/null +++ b/src/helpers/integrations/get_integrations.ts @@ -0,0 +1,14 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Returns a list of integrations for the guild. Requires the MANAGE_GUILD permission. */ +export async function getIntegrations(guildID: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const result = await RequestManager.get( + endpoints.GUILD_INTEGRATIONS(guildID), + ); + + return result; +} diff --git a/src/helpers/integrations/sync_integration.ts b/src/helpers/integrations/sync_integration.ts new file mode 100644 index 000000000..43d4e0fa5 --- /dev/null +++ b/src/helpers/integrations/sync_integration.ts @@ -0,0 +1,14 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Sync an integration. Requires the MANAGE_GUILD permission. */ +export async function syncIntegration(guildID: string, id: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const result = await RequestManager.post( + endpoints.GUILD_INTEGRATION_SYNC(guildID, id), + ); + + return result; +} diff --git a/src/helpers/invites/create_invite.ts b/src/helpers/invites/create_invite.ts new file mode 100644 index 000000000..73742b421 --- /dev/null +++ b/src/helpers/invites/create_invite.ts @@ -0,0 +1,33 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { CreateInviteOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Creates a new invite for this channel. Requires CREATE_INSTANT_INVITE */ +export async function createInvite( + channelID: string, + options: CreateInviteOptions, +) { + await requireBotChannelPermissions(channelID, ["CREATE_INSTANT_INVITE"]); + + if (options.max_age && (options.max_age > 604800 || options.max_age < 0)) { + console.log( + `The max age for invite created in ${channelID} was not between 0-604800. Using default values instead.`, + ); + options.max_age = undefined; + } + + if (options.max_uses && (options.max_uses > 100 || options.max_uses < 0)) { + console.log( + `The max uses for invite created in ${channelID} was not between 0-100. Using default values instead.`, + ); + options.max_uses = undefined; + } + + const result = await RequestManager.post( + endpoints.CHANNEL_INVITES(channelID), + options, + ); + + return result; +} diff --git a/src/helpers/invites/delete_invite.ts b/src/helpers/invites/delete_invite.ts new file mode 100644 index 000000000..d5b055126 --- /dev/null +++ b/src/helpers/invites/delete_invite.ts @@ -0,0 +1,27 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors, InvitePayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + botHasChannelPermissions, + requireBotGuildPermissions, +} from "../../util/permissions.ts"; + +/** Deletes an invite for the given code. Requires `MANAGE_CHANNELS` or `MANAGE_GUILD` permission */ +export async function deleteInvite(channelID: string, inviteCode: string) { + const channel = await cacheHandlers.get("channels", channelID); + + if (!channel) throw new Error(Errors.CHANNEL_NOT_FOUND); + + const hasPerm = await botHasChannelPermissions(channel, [ + "MANAGE_CHANNELS", + ]); + + if (!hasPerm) { + await requireBotGuildPermissions(channel!.guildID, ["MANAGE_GUILD"]); + } + + const result = await RequestManager.delete(endpoints.INVITE(inviteCode)); + + return result as InvitePayload; +} diff --git a/src/helpers/invites/get_channel_invites.ts b/src/helpers/invites/get_channel_invites.ts new file mode 100644 index 000000000..65cac8155 --- /dev/null +++ b/src/helpers/invites/get_channel_invites.ts @@ -0,0 +1,12 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Gets the invites for this channel. Requires MANAGE_CHANNEL */ +export async function getChannelInvites(channelID: string) { + await requireBotChannelPermissions(channelID, ["MANAGE_CHANNELS"]); + + const result = await RequestManager.get(endpoints.CHANNEL_INVITES(channelID)); + + return result; +} diff --git a/src/helpers/invites/get_invite.ts b/src/helpers/invites/get_invite.ts new file mode 100644 index 000000000..02dc66dca --- /dev/null +++ b/src/helpers/invites/get_invite.ts @@ -0,0 +1,10 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { InvitePayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns an invite for the given code. */ +export async function getInvite(inviteCode: string) { + const result = await RequestManager.get(endpoints.INVITE(inviteCode)); + + return result as InvitePayload; +} diff --git a/src/helpers/invites/get_invites.ts b/src/helpers/invites/get_invites.ts new file mode 100644 index 000000000..8dc9ed713 --- /dev/null +++ b/src/helpers/invites/get_invites.ts @@ -0,0 +1,12 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Get all the invites for this guild. Requires MANAGE_GUILD permission */ +export async function getInvites(guildID: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const result = await RequestManager.get(endpoints.GUILD_INVITES(guildID)); + + return result; +} diff --git a/src/helpers/members/avatar_url.ts b/src/helpers/members/avatar_url.ts new file mode 100644 index 000000000..bfb5da949 --- /dev/null +++ b/src/helpers/members/avatar_url.ts @@ -0,0 +1,18 @@ +import { Member } from "../../structures/mod.ts"; +import { ImageFormats, ImageSize } from "../../types/mod.ts"; +import { rawAvatarURL } from "./raw_avatar_url.ts"; + +/** The users custom avatar or the default avatar */ +export function avatarURL( + member: Member, + size: ImageSize = 128, + format?: ImageFormats, +) { + return rawAvatarURL( + member.id, + member.discriminator, + member.avatar, + size, + format, + ); +} diff --git a/src/helpers/members/ban_member.ts b/src/helpers/members/ban_member.ts new file mode 100644 index 000000000..4a08d8fb4 --- /dev/null +++ b/src/helpers/members/ban_member.ts @@ -0,0 +1,19 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { BanOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Ban a user from the guild and optionally delete previous messages sent by the user. Requires the BAN_MEMBERS permission. */ +export async function ban(guildID: string, id: string, options: BanOptions) { + await requireBotGuildPermissions(guildID, ["BAN_MEMBERS"]); + + const result = await RequestManager.put(endpoints.GUILD_BAN(guildID, id), { + ...options, + delete_message_days: options.days, + }); + + return result; +} + +// aliases +export { ban as banMember }; diff --git a/src/helpers/members/disconnect_member.ts b/src/helpers/members/disconnect_member.ts new file mode 100644 index 000000000..d29b96485 --- /dev/null +++ b/src/helpers/members/disconnect_member.ts @@ -0,0 +1,9 @@ +import { editMember } from "./edit_member.ts"; + +/** Kicks a member from a voice channel */ +export function kickFromVoiceChannel(guildID: string, memberID: string) { + return editMember(guildID, memberID, { channel_id: null }); +} + +// aliases +export { kickFromVoiceChannel as disconnectMember }; diff --git a/src/helpers/members/edit_bot_nickname.ts b/src/helpers/members/edit_bot_nickname.ts new file mode 100644 index 000000000..66cdc206e --- /dev/null +++ b/src/helpers/members/edit_bot_nickname.ts @@ -0,0 +1,18 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Edit the nickname of the bot in this guild */ +export async function editBotNickname( + guildID: string, + nickname: string | null, +) { + await requireBotGuildPermissions(guildID, ["CHANGE_NICKNAME"]); + + const response = await RequestManager.patch( + endpoints.USER_NICK(guildID), + { nick: nickname }, + ) as { nick: string }; + + return response.nick; +} diff --git a/src/helpers/members/edit_bot_profile.ts b/src/helpers/members/edit_bot_profile.ts new file mode 100644 index 000000000..4a438e279 --- /dev/null +++ b/src/helpers/members/edit_bot_profile.ts @@ -0,0 +1,38 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { urlToBase64 } from "../../util/utils.ts"; + +/** Modifies the bot's username or avatar. + * NOTE: username: if changed may cause the bot's discriminator to be randomized. + */ +export async function editBotProfile(username?: string, botAvatarURL?: string) { + // Nothing was edited + if (!username && !botAvatarURL) return; + // Check username requirements if username was provided + if (username) { + if (username.length > 32) { + throw new Error(Errors.USERNAME_MAX_LENGTH); + } + if (username.length < 2) { + throw new Error(Errors.USERNAME_MIN_LENGTH); + } + if (["@", "#", ":", "```"].some((char) => username.includes(char))) { + throw new Error(Errors.USERNAME_INVALID_CHARACTER); + } + if (["discordtag", "everyone", "here"].includes(username)) { + throw new Error(Errors.USERNAME_INVALID_USERNAME); + } + } + + const avatar = botAvatarURL ? await urlToBase64(botAvatarURL) : undefined; + const result = await RequestManager.patch( + endpoints.USER_BOT, + { + username: username?.trim(), + avatar, + }, + ); + + return result; +} diff --git a/src/helpers/members/edit_member.ts b/src/helpers/members/edit_member.ts new file mode 100644 index 000000000..e08bb7b7b --- /dev/null +++ b/src/helpers/members/edit_member.ts @@ -0,0 +1,80 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { + EditMemberOptions, + Errors, + MemberCreatePayload, + Permission, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + requireBotChannelPermissions, + requireBotGuildPermissions, +} from "../../util/permissions.ts"; + +/** Edit the member */ +export async function editMember( + guildID: string, + memberID: string, + options: EditMemberOptions, +) { + const requiredPerms: Set = new Set(); + + if (options.nick) { + if (options.nick.length > 32) { + throw new Error(Errors.NICKNAMES_MAX_LENGTH); + } + requiredPerms.add("MANAGE_NICKNAMES"); + } + + if (options.roles) requiredPerms.add("MANAGE_ROLES"); + + if ( + typeof options.mute !== "undefined" || + typeof options.deaf !== "undefined" || + (typeof options.channel_id !== "undefined" || "null") + ) { + const memberVoiceState = (await cacheHandlers.get("guilds", guildID)) + ?.voiceStates.get(memberID); + + if (!memberVoiceState?.channelID) { + throw new Error(Errors.MEMBER_NOT_IN_VOICE_CHANNEL); + } + + if (typeof options.mute !== "undefined") { + requiredPerms.add("MUTE_MEMBERS"); + } + + if (typeof options.deaf !== "undefined") { + requiredPerms.add("DEAFEN_MEMBERS"); + } + + if (options.channel_id) { + const requiredVoicePerms: Set = new Set([ + "CONNECT", + "MOVE_MEMBERS", + ]); + if (memberVoiceState) { + await requireBotChannelPermissions( + memberVoiceState?.channelID, + [...requiredVoicePerms], + ); + } + await requireBotChannelPermissions( + options.channel_id, + [...requiredVoicePerms], + ); + } + } + + await requireBotGuildPermissions(guildID, [...requiredPerms]); + + const result = await RequestManager.patch( + endpoints.GUILD_MEMBER(guildID, memberID), + options, + ) as MemberCreatePayload; + const member = await structures.createMemberStruct(result, guildID); + + return member; +} diff --git a/src/helpers/members/fetch_members.ts b/src/helpers/members/fetch_members.ts new file mode 100644 index 000000000..342735c28 --- /dev/null +++ b/src/helpers/members/fetch_members.ts @@ -0,0 +1,31 @@ +import { identifyPayload } from "../../bot.ts"; +import { Guild, Member } from "../../structures/mod.ts"; +import { Errors, FetchMembersOptions, Intents } from "../../types/mod.ts"; +import { Collection } from "../../util/collection.ts"; +import { requestAllMembers } from "../../ws/shard_manager.ts"; + +/** + * ⚠️ BEGINNER DEVS!! YOU SHOULD ALMOST NEVER NEED THIS AND YOU CAN GET FROM cache.members.get() + * + * ADVANCED: + * Highly recommended to use this function to fetch members instead of getMember from REST. + * REST: 50/s global(across all shards) rate limit with ALL requests this included + * GW(this function): 120/m(PER shard) rate limit. Meaning if you have 8 shards your limit is now 960/m. + */ +export function fetchMembers(guild: Guild, options?: FetchMembersOptions) { + // You can request 1 member without the intent + if ( + (!options?.limit || options.limit > 1) && + !(identifyPayload.intents && Intents.GUILD_MEMBERS) + ) { + throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS); + } + + if (options?.userIDs?.length) { + options.limit = options.userIDs.length; + } + + return new Promise((resolve) => { + return requestAllMembers(guild, resolve, options); + }) as Promise>; +} diff --git a/src/helpers/members/get_member.ts b/src/helpers/members/get_member.ts new file mode 100644 index 000000000..4b182db51 --- /dev/null +++ b/src/helpers/members/get_member.ts @@ -0,0 +1,27 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { MemberCreatePayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns a guild member object for the specified user. + * + * ⚠️ **ADVANCED USE ONLY: Your members will be cached in your guild most likely. Only use this when you are absolutely sure the member is not cached.** + */ +export async function getMember( + guildID: string, + id: string, + options?: { force?: boolean }, +) { + const guild = await cacheHandlers.get("guilds", guildID); + if (!guild && !options?.force) return; + + const data = (await RequestManager.get( + endpoints.GUILD_MEMBER(guildID, id), + )) as MemberCreatePayload; + + const memberStruct = await structures.createMemberStruct(data, guildID); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + return memberStruct; +} diff --git a/src/helpers/members/get_members.ts b/src/helpers/members/get_members.ts new file mode 100644 index 000000000..f91d70ecd --- /dev/null +++ b/src/helpers/members/get_members.ts @@ -0,0 +1,82 @@ +import { identifyPayload } from "../../bot.ts"; +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Member, structures } from "../../structures/mod.ts"; +import { + Errors, + GetMemberOptions, + Intents, + MemberCreatePayload, +} from "../../types/mod.ts"; +import { Collection } from "../../util/collection.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** + * ⚠️ BEGINNER DEVS!! YOU SHOULD ALMOST NEVER NEED THIS AND YOU CAN GET FROM cache.members.get() + * + * ADVANCED: + * Highly recommended to **NOT** use this function to get members instead use fetchMembers(). + * REST(this function): 50/s global(across all shards) rate limit with ALL requests this included + * GW(fetchMembers): 120/m(PER shard) rate limit. Meaning if you have 8 shards your limit is 960/m. + */ +export async function getMembers(guildID: string, options?: GetMemberOptions) { + if (!(identifyPayload.intents && Intents.GUILD_MEMBERS)) { + throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS); + } + + const guild = await cacheHandlers.get("guilds", guildID); + if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); + + const members = new Collection(); + + let membersLeft = options?.limit ?? guild.memberCount; + let loops = 1; + while ( + (options?.limit ?? guild.memberCount) > members.size && + membersLeft > 0 + ) { + if (options?.limit && options.limit > 1000) { + console.log( + `Paginating get members from REST. #${loops} / ${ + Math.ceil( + (options?.limit ?? 1) / 1000, + ) + }`, + ); + } + + const result = (await RequestManager.get( + `${endpoints.GUILD_MEMBERS(guildID)}?limit=${ + membersLeft > 1000 ? 1000 : membersLeft + }${options?.after ? `&after=${options.after}` : ""}`, + )) as MemberCreatePayload[]; + + const memberStructures = await Promise.all( + result.map(async (member) => { + const memberStruct = await structures.createMemberStruct( + member, + guildID, + ); + + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + return memberStruct; + }), + ) as Member[]; + + if (!memberStructures.length) break; + + memberStructures.forEach((member) => members.set(member.id, member)); + + options = { + limit: options?.limit, + after: memberStructures[memberStructures.length - 1].id, + }; + + membersLeft -= 1000; + + loops++; + } + + return members; +} diff --git a/src/helpers/members/get_members_by_query.ts b/src/helpers/members/get_members_by_query.ts new file mode 100644 index 000000000..a991e53b7 --- /dev/null +++ b/src/helpers/members/get_members_by_query.ts @@ -0,0 +1,21 @@ +import { cacheHandlers } from "../../cache.ts"; +import { Member } from "../../structures/mod.ts"; +import { Collection } from "../../util/collection.ts"; +import { requestAllMembers } from "../../ws/shard_manager.ts"; + +/** Returns guild member objects for the specified user by their nickname/username. + * + * ⚠️ **ADVANCED USE ONLY: Your members will be cached in your guild most likely. Only use this when you are absolutely sure the member is not cached.** + */ +export async function getMembersByQuery( + guildID: string, + name: string, + limit = 1, +) { + const guild = await cacheHandlers.get("guilds", guildID); + if (!guild) return; + + return new Promise((resolve) => { + return requestAllMembers(guild, resolve, { query: name, limit }); + }) as Promise>; +} diff --git a/src/helpers/members/kick_member.ts b/src/helpers/members/kick_member.ts new file mode 100644 index 000000000..6783cfe6b --- /dev/null +++ b/src/helpers/members/kick_member.ts @@ -0,0 +1,32 @@ +import { botID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + highestRole, + requireBotGuildPermissions, +} from "../../util/permissions.ts"; + +/** Kick a member from the server */ +export async function kick(guildID: string, memberID: string, reason?: string) { + const botsHighestRole = await highestRole(guildID, botID); + const membersHighestRole = await highestRole(guildID, memberID); + if ( + botsHighestRole && membersHighestRole && + botsHighestRole.position <= membersHighestRole.position + ) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + } + + await requireBotGuildPermissions(guildID, ["KICK_MEMBERS"]); + + const result = await RequestManager.delete( + endpoints.GUILD_MEMBER(guildID, memberID), + { reason }, + ); + + return result; +} + +// aliases +export { kick as kickMember }; diff --git a/src/helpers/members/move_member.ts b/src/helpers/members/move_member.ts new file mode 100644 index 000000000..504cd971e --- /dev/null +++ b/src/helpers/members/move_member.ts @@ -0,0 +1,15 @@ +import { editMember } from "./edit_member.ts"; + +/** + * Move a member from a voice channel to another. + * @param guildID the id of the guild which the channel exists in + * @param memberID the id of the member to move. + * @param channelID id of channel to move user to (if they are connected to voice) + */ +export function moveMember( + guildID: string, + memberID: string, + channelID: string, +) { + return editMember(guildID, memberID, { channel_id: channelID }); +} diff --git a/src/helpers/members/prune_members.ts b/src/helpers/members/prune_members.ts new file mode 100644 index 000000000..7f3fadce7 --- /dev/null +++ b/src/helpers/members/prune_members.ts @@ -0,0 +1,27 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors, PruneOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; +import { camelKeysToSnakeCase } from "../../util/utils.ts"; + +/** + * Begin a prune operation. Requires the KICK_MEMBERS permission. Returns an object with one 'pruned' key indicating the number of members that were removed in the prune operation. For large guilds it's recommended to set the computePruneCount option to false, forcing 'pruned' to null. Fires multiple Guild Member Remove Gateway events. + * + * By default, prune will not remove users with roles. You can optionally include specific roles in your prune by providing the roles (resolved to include_roles internally) parameter. Any inactive user that has a subset of the provided role(s) will be included in the prune and users with additional roles will not. + */ +export async function pruneMembers( + guildID: string, + options: PruneOptions, +) { + if (options.days && options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); + if (options.days && options.days > 30) throw new Error(Errors.PRUNE_MAX_DAYS); + + await requireBotGuildPermissions(guildID, ["KICK_MEMBERS"]); + + const result = await RequestManager.post( + endpoints.GUILD_PRUNE(guildID), + camelKeysToSnakeCase(options), + ); + + return result; +} diff --git a/src/helpers/members/raw_avatar_url.ts b/src/helpers/members/raw_avatar_url.ts new file mode 100644 index 000000000..d10a8956f --- /dev/null +++ b/src/helpers/members/raw_avatar_url.ts @@ -0,0 +1,16 @@ +import { ImageFormats, ImageSize } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { formatImageURL } from "../../util/utils.ts"; + +/** The users custom avatar or the default avatar if you don't have a member object. */ +export function rawAvatarURL( + userID: string, + discriminator: string, + avatar?: string | null, + size: ImageSize = 128, + format?: ImageFormats, +) { + return avatar + ? formatImageURL(endpoints.USER_AVATAR(userID, avatar), size, format) + : endpoints.USER_DEFAULT_AVATAR(Number(discriminator) % 5); +} diff --git a/src/helpers/members/send_direct_message.ts b/src/helpers/members/send_direct_message.ts new file mode 100644 index 000000000..dca5a4472 --- /dev/null +++ b/src/helpers/members/send_direct_message.ts @@ -0,0 +1,34 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { + ChannelCreatePayload, + DMChannelCreatePayload, + MessageContent, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { sendMessage } from "../messages/send_message.ts"; + +/** Send a message to a users DM. Note: this takes 2 API calls. 1 is to fetch the users dm channel. 2 is to send a message to that channel. */ +export async function sendDirectMessage( + memberID: string, + content: string | MessageContent, +) { + let dmChannel = await cacheHandlers.get("channels", memberID); + if (!dmChannel) { + // If not available in cache create a new one. + const dmChannelData = await RequestManager.post( + endpoints.USER_DM, + { recipient_id: memberID }, + ) as DMChannelCreatePayload; + const channelStruct = await structures.createChannelStruct( + dmChannelData as unknown as ChannelCreatePayload, + ); + // Recreate the channel and add it undert he users id + await cacheHandlers.set("channels", memberID, channelStruct); + dmChannel = channelStruct; + } + + // If it does exist try sending a message to this user + return sendMessage(dmChannel.id, content); +} diff --git a/src/helpers/members/unban_member.ts b/src/helpers/members/unban_member.ts new file mode 100644 index 000000000..f3149454f --- /dev/null +++ b/src/helpers/members/unban_member.ts @@ -0,0 +1,15 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Remove the ban for a user. Requires BAN_MEMBERS permission */ +export async function unban(guildID: string, id: string) { + await requireBotGuildPermissions(guildID, ["BAN_MEMBERS"]); + + const result = await RequestManager.delete(endpoints.GUILD_BAN(guildID, id)); + + return result; +} + +// aliases +export { unban as unbanMember }; diff --git a/src/helpers/messages/add_reaction.ts b/src/helpers/messages/add_reaction.ts new file mode 100644 index 000000000..0e8009fb7 --- /dev/null +++ b/src/helpers/messages/add_reaction.ts @@ -0,0 +1,27 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Create a reaction for the message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. Requires READ_MESSAGE_HISTORY and ADD_REACTIONS */ +export async function addReaction( + channelID: string, + messageID: string, + reaction: string, +) { + await requireBotChannelPermissions(channelID, [ + "ADD_REACTIONS", + "READ_MESSAGE_HISTORY", + ]); + + if (reaction.startsWith("<:")) { + reaction = reaction.substring(2, reaction.length - 1); + } else if (reaction.startsWith(" addReaction(channelID, messageID, reaction)), + ); + } else { + for (const reaction of reactions) { + await addReaction(channelID, messageID, reaction); + } + } +} diff --git a/src/helpers/messages/delete_message.ts b/src/helpers/messages/delete_message.ts new file mode 100644 index 000000000..a1f5672ff --- /dev/null +++ b/src/helpers/messages/delete_message.ts @@ -0,0 +1,29 @@ +import { botID } from "../../bot.ts"; +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; +import { delay } from "../../util/utils.ts"; + +/** Delete a message with the channel id and message id only. */ +export async function deleteMessage( + channelID: string, + messageID: string, + reason?: string, + delayMilliseconds = 0, +) { + const message = await cacheHandlers.get("messages", messageID); + + if (message && message.author.id !== botID) { + await requireBotChannelPermissions(message.channelID, ["MANAGE_MESSAGES"]); + } + + if (delayMilliseconds) await delay(delayMilliseconds); + + const result = await RequestManager.delete( + endpoints.CHANNEL_MESSAGE(channelID, messageID), + { reason }, + ); + + return result; +} diff --git a/src/helpers/messages/delete_messages.ts b/src/helpers/messages/delete_messages.ts new file mode 100644 index 000000000..662e7dff4 --- /dev/null +++ b/src/helpers/messages/delete_messages.ts @@ -0,0 +1,33 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Delete messages from the channel. 2-100. Requires the MANAGE_MESSAGES permission */ +export async function deleteMessages( + channelID: string, + ids: string[], + reason?: string, +) { + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); + + if (ids.length < 2) { + throw new Error(Errors.DELETE_MESSAGES_MIN); + } + + if (ids.length > 100) { + console.warn( + `This endpoint only accepts a maximum of 100 messages. Deleting the first 100 message ids provided.`, + ); + } + + const result = await RequestManager.post( + endpoints.CHANNEL_BULK_DELETE(channelID), + { + messages: ids.splice(0, 100), + reason, + }, + ); + + return result; +} diff --git a/src/helpers/messages/edit_message.ts b/src/helpers/messages/edit_message.ts new file mode 100644 index 000000000..92899f0c3 --- /dev/null +++ b/src/helpers/messages/edit_message.ts @@ -0,0 +1,40 @@ +import { botID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Message, structures } from "../../structures/mod.ts"; +import { + Errors, + MessageContent, + MessageCreateOptions, + Permission, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Edit the message. */ +export async function editMessage( + message: Message, + content: string | MessageContent, +) { + if (message.author.id !== botID) { + throw "You can only edit a message that was sent by the bot."; + } + + if (typeof content === "string") content = { content }; + + const requiredPerms: Permission[] = ["SEND_MESSAGES"]; + + if (content.tts) requiredPerms.push("SEND_TTS_MESSAGES"); + + await requireBotChannelPermissions(message.channelID, requiredPerms); + + if (content.content && content.content.length > 2000) { + throw new Error(Errors.MESSAGE_MAX_LENGTH); + } + + const result = await RequestManager.patch( + endpoints.CHANNEL_MESSAGE(message.channelID, message.id), + content, + ); + + return structures.createMessageStruct(result as MessageCreateOptions); +} diff --git a/src/helpers/messages/get_message.ts b/src/helpers/messages/get_message.ts new file mode 100644 index 000000000..1b285b8ff --- /dev/null +++ b/src/helpers/messages/get_message.ts @@ -0,0 +1,19 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { MessageCreateOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Fetch a single message from the server. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */ +export async function getMessage(channelID: string, id: string) { + await requireBotChannelPermissions(channelID, [ + "VIEW_CHANNEL", + "READ_MESSAGE_HISTORY", + ]); + + const result = (await RequestManager.get( + endpoints.CHANNEL_MESSAGE(channelID, id), + )) as MessageCreateOptions; + + return structures.createMessageStruct(result); +} diff --git a/src/helpers/messages/get_messages.ts b/src/helpers/messages/get_messages.ts new file mode 100644 index 000000000..1de5ced70 --- /dev/null +++ b/src/helpers/messages/get_messages.ts @@ -0,0 +1,37 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { + GetMessages, + GetMessagesAfter, + GetMessagesAround, + GetMessagesBefore, + MessageCreateOptions, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Fetches between 2-100 messages. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */ +export async function getMessages( + channelID: string, + options?: + | GetMessagesAfter + | GetMessagesBefore + | GetMessagesAround + | GetMessages, +) { + await requireBotChannelPermissions(channelID, [ + "VIEW_CHANNEL", + "READ_MESSAGE_HISTORY", + ]); + + if (options?.limit && options.limit > 100) return; + + const result = (await RequestManager.get( + endpoints.CHANNEL_MESSAGES(channelID), + options, + )) as MessageCreateOptions[]; + + return Promise.all( + result.map((res) => structures.createMessageStruct(res)), + ); +} diff --git a/src/helpers/messages/get_reactions.ts b/src/helpers/messages/get_reactions.ts new file mode 100644 index 000000000..c05ae2e1e --- /dev/null +++ b/src/helpers/messages/get_reactions.ts @@ -0,0 +1,19 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { DiscordGetReactionsParams, UserPayload } from "../../types/mod.ts"; +import { Collection } from "../../util/collection.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Get a list of users that reacted with this emoji. */ +export async function getReactions( + channelID: string, + messageID: string, + reaction: string, + options?: DiscordGetReactionsParams, +) { + const users = (await RequestManager.get( + endpoints.CHANNEL_MESSAGE_REACTION(channelID, messageID, reaction), + options, + )) as UserPayload[]; + + return new Collection(users.map((user) => [user.id, user])); +} diff --git a/src/helpers/messages/pin_message.ts b/src/helpers/messages/pin_message.ts new file mode 100644 index 000000000..65cdf8161 --- /dev/null +++ b/src/helpers/messages/pin_message.ts @@ -0,0 +1,17 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Pin a message in a channel. Requires MANAGE_MESSAGES. Max pins allowed in a channel = 50. */ +export async function pin(channelID: string, messageID: string) { + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); + + const result = await RequestManager.put( + endpoints.CHANNEL_PIN(channelID, messageID), + ); + + return result; +} + +// aliases +export { pin as pinMessage }; diff --git a/src/helpers/messages/publish_message.ts b/src/helpers/messages/publish_message.ts new file mode 100644 index 000000000..92f781e00 --- /dev/null +++ b/src/helpers/messages/publish_message.ts @@ -0,0 +1,13 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { MessageCreateOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Crosspost a message in a News Channel to following channels. */ +export async function publishMessage(channelID: string, messageID: string) { + const data = (await RequestManager.post( + endpoints.CHANNEL_MESSAGE_CROSSPOST(channelID, messageID), + )) as MessageCreateOptions; + + return structures.createMessageStruct(data); +} diff --git a/src/helpers/messages/remove_all_reactions.ts b/src/helpers/messages/remove_all_reactions.ts new file mode 100644 index 000000000..5fd6ddea6 --- /dev/null +++ b/src/helpers/messages/remove_all_reactions.ts @@ -0,0 +1,14 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Removes all reactions for all emojis on this message. */ +export async function removeAllReactions(channelID: string, messageID: string) { + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); + + const result = await RequestManager.delete( + endpoints.CHANNEL_MESSAGE_REACTIONS(channelID, messageID), + ); + + return result; +} diff --git a/src/helpers/messages/remove_reaction.ts b/src/helpers/messages/remove_reaction.ts new file mode 100644 index 000000000..09de64090 --- /dev/null +++ b/src/helpers/messages/remove_reaction.ts @@ -0,0 +1,21 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Removes a reaction from the bot on this message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. */ +export async function removeReaction( + channelID: string, + messageID: string, + reaction: string, +) { + if (reaction.startsWith("<:")) { + reaction = reaction.substring(2, reaction.length - 1); + } else if (reaction.startsWith(" = new Set([ + "SEND_MESSAGES", + "VIEW_CHANNEL", + ]); + + if (content.tts) requiredPerms.add("SEND_TTS_MESSAGES"); + if (content.embed) requiredPerms.add("EMBED_LINKS"); + if (content.replyMessageID || content.mentions?.repliedUser) { + requiredPerms.add("READ_MESSAGE_HISTORY"); + } + + await requireBotChannelPermissions(channelID, [...requiredPerms]); + } + + // Use ... for content length due to unicode characters and js .length handling + if (content.content && [...content.content].length > 2000) { + throw new Error(Errors.MESSAGE_MAX_LENGTH); + } + + if (content.mentions) { + if (content.mentions.users?.length) { + if (content.mentions.parse?.includes("users")) { + content.mentions.parse = content.mentions.parse.filter( + (p) => p !== "users", + ); + } + + if (content.mentions.users.length > 100) { + content.mentions.users = content.mentions.users.slice(0, 100); + } + } + + if (content.mentions.roles?.length) { + if (content.mentions.parse?.includes("roles")) { + content.mentions.parse = content.mentions.parse.filter( + (p) => p !== "roles", + ); + } + + if (content.mentions.roles.length > 100) { + content.mentions.roles = content.mentions.roles.slice(0, 100); + } + } + } + + const result = (await RequestManager.post( + endpoints.CHANNEL_MESSAGES(channelID), + { + ...content, + allowed_mentions: content.mentions + ? { + ...content.mentions, + replied_user: content.mentions.repliedUser, + } + : undefined, + ...(content.replyMessageID + ? { + message_reference: { + message_id: content.replyMessageID, + fail_if_not_exists: content.failReplyIfNotExists === true, + }, + } + : {}), + }, + )) as MessageCreateOptions; + + return structures.createMessageStruct(result); +} diff --git a/src/helpers/messages/unpin_message.ts b/src/helpers/messages/unpin_message.ts new file mode 100644 index 000000000..0f4179142 --- /dev/null +++ b/src/helpers/messages/unpin_message.ts @@ -0,0 +1,17 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Unpin a message in a channel. Requires MANAGE_MESSAGES. */ +export async function unpin(channelID: string, messageID: string) { + await requireBotChannelPermissions(channelID, ["MANAGE_MESSAGES"]); + + const result = await RequestManager.delete( + endpoints.CHANNEL_PIN(channelID, messageID), + ); + + return result; +} + +// aliases +export { unpin as unpinMessage }; diff --git a/src/api/handlers/gateway.ts b/src/helpers/misc/get_gateway_bot.ts similarity index 100% rename from src/api/handlers/gateway.ts rename to src/helpers/misc/get_gateway_bot.ts diff --git a/src/helpers/misc/get_user.ts b/src/helpers/misc/get_user.ts new file mode 100644 index 000000000..1d9ba369f --- /dev/null +++ b/src/helpers/misc/get_user.ts @@ -0,0 +1,10 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { UserPayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** This function will return the raw user payload in the rare cases you need to fetch a user directly from the API. */ +export async function getUser(userID: string) { + const result = await RequestManager.get(endpoints.USER(userID)); + + return result as UserPayload; +} diff --git a/src/helpers/mod.ts b/src/helpers/mod.ts new file mode 100644 index 000000000..b6fed9609 --- /dev/null +++ b/src/helpers/mod.ts @@ -0,0 +1,381 @@ +import { categoryChildrenIDs } from "./channels/category_children_ids.ts"; +import { channelOverwriteHasPermission } from "./channels/channel_overwrite_has_permission.ts"; +import { createGuildChannel } from "./channels/create_guild_channel.ts"; +import { deleteChannel } from "./channels/delete_channel.ts"; +import { deleteChannelOverwrite } from "./channels/delete_channel_overwrite.ts"; +import { editChannel } from "./channels/edit_channel.ts"; +import { editChannelOverwrite } from "./channels/edit_channel_overwrite.ts"; +import { followChannel } from "./channels/follow_channel.ts"; +import { getChannel } from "./channels/get_channel.ts"; +import { getChannels } from "./channels/get_channels.ts"; +import { getChannelWebhooks } from "./channels/get_channel_webhooks.ts"; +import { getPins } from "./channels/get_pins.ts"; +import { isChannelSynced } from "./channels/is_channel_synced.ts"; +import { startTyping } from "./channels/start_typing.ts"; +import { swapChannels } from "./channels/swap_channels.ts"; +import { createSlashCommand } from "./commands/create_slash_command.ts"; +import { deleteSlashCommand } from "./commands/delete_slash_command.ts"; +import { deleteSlashResponse } from "./commands/delete_slash_response.ts"; +import { editSlashResponse } from "./commands/edit_slash_response.ts"; +import { executeSlashCommand } from "./commands/execute_slash_command.ts"; +import { getSlashCommand } from "./commands/get_slash_command.ts"; +import { getSlashCommands } from "./commands/get_slash_commands.ts"; +import { upsertSlashCommand } from "./commands/upsert_slash_command.ts"; +import { upsertSlashCommands } from "./commands/upsert_slash_commands.ts"; +import { createEmoji } from "./emojis/create_emoji.ts"; +import { deleteEmoji } from "./emojis/delete_emoji.ts"; +import { editEmoji } from "./emojis/edit_emoji.ts"; +import { emojiURL } from "./emojis/emoji_url.ts"; +import { getEmoji } from "./emojis/get_emoji.ts"; +import { getEmojis } from "./emojis/get_emojis.ts"; +import { createGuild } from "./guilds/create_guild.ts"; +import { deleteServer } from "./guilds/delete_server.ts"; +import { editGuild } from "./guilds/edit_guild.ts"; +import { editWidget } from "./guilds/edit_widget.ts"; +import { getAuditLogs } from "./guilds/get_audit_logs.ts"; +import { getAvailableVoiceRegions } from "./guilds/get_available_voice_regions.ts"; +import { getBan } from "./guilds/get_ban.ts"; +import { getBans } from "./guilds/get_bans.ts"; +import { getGuild } from "./guilds/get_guild.ts"; +import { getGuildPreview } from "./guilds/get_guild_preview.ts"; +import { getPruneCount } from "./guilds/get_prune_count.ts"; +import { getVanityURL } from "./guilds/get_vainty_url.ts"; +import { getVoiceRegions } from "./guilds/get_voice_regions.ts"; +import { getWidget } from "./guilds/get_widget.ts"; +import { getWidgetImageURL } from "./guilds/get_widget_image_url.ts"; +import { getWidgetSettings } from "./guilds/get_widget_settings.ts"; +import { guildBannerURL } from "./guilds/guild_banner_url.ts"; +import { guildIconURL } from "./guilds/guild_icon_url.ts"; +import { guildSplashURL } from "./guilds/guild_splash_url.ts"; +import { leaveGuild } from "./guilds/leave_guild.ts"; +import { deleteIntegration } from "./integrations/delete_integration.ts"; +import { editIntegration } from "./integrations/edit_integration.ts"; +import { getIntegrations } from "./integrations/get_integrations.ts"; +import { syncIntegration } from "./integrations/sync_integration.ts"; +import { createInvite } from "./invites/create_invite.ts"; +import { deleteInvite } from "./invites/delete_invite.ts"; +import { getChannelInvites } from "./invites/get_channel_invites.ts"; +import { getInvite } from "./invites/get_invite.ts"; +import { getInvites } from "./invites/get_invites.ts"; +import { avatarURL } from "./members/avatar_url.ts"; +import { banMember } from "./members/ban_member.ts"; +import { disconnectMember } from "./members/disconnect_member.ts"; +import { editBotNickname } from "./members/edit_bot_nickname.ts"; +import { editBotProfile } from "./members/edit_bot_profile.ts"; +import { editMember } from "./members/edit_member.ts"; +import { fetchMembers } from "./members/fetch_members.ts"; +import { getMember } from "./members/get_member.ts"; +import { getMembers } from "./members/get_members.ts"; +import { getMembersByQuery } from "./members/get_members_by_query.ts"; +import { kickMember } from "./members/kick_member.ts"; +import { moveMember } from "./members/move_member.ts"; +import { pruneMembers } from "./members/prune_members.ts"; +import { rawAvatarURL } from "./members/raw_avatar_url.ts"; +import { sendDirectMessage } from "./members/send_direct_message.ts"; +import { unbanMember } from "./members/unban_member.ts"; +import { addReaction } from "./messages/add_reaction.ts"; +import { addReactions } from "./messages/add_reactions.ts"; +import { deleteMessage } from "./messages/delete_message.ts"; +import { deleteMessages } from "./messages/delete_messages.ts"; +import { editMessage } from "./messages/edit_message.ts"; +import { getMessage } from "./messages/get_message.ts"; +import { getMessages } from "./messages/get_messages.ts"; +import { getReactions } from "./messages/get_reactions.ts"; +import { pinMessage } from "./messages/pin_message.ts"; +import { publishMessage } from "./messages/publish_message.ts"; +import { removeAllReactions } from "./messages/remove_all_reactions.ts"; +import { removeReaction } from "./messages/remove_reaction.ts"; +import { removeReactionEmoji } from "./messages/remove_reaction_emoji.ts"; +import { removeUserReaction } from "./messages/remove_user_reaction.ts"; +import { sendMessage } from "./messages/send_message.ts"; +import { unpinMessage } from "./messages/unpin_message.ts"; +import { getGatewayBot } from "./misc/get_gateway_bot.ts"; +import { getUser } from "./misc/get_user.ts"; +import { addRole } from "./roles/add_role.ts"; +import { createRole } from "./roles/create_role.ts"; +import { deleteRole } from "./roles/delete_role.ts"; +import { editRole } from "./roles/edit_role.ts"; +import { getRoles } from "./roles/get_roles.ts"; +import { removeRole } from "./roles/remove_role.ts"; +import { swapRoles } from "./roles/swap_roles.ts"; +import { createGuildFromTemplate } from "./templates/create_guild_from_template.ts"; +import { createGuildTemplate } from "./templates/create_guild_template.ts"; +import { deleteGuildTemplate } from "./templates/delete_guild_template.ts"; +import { editGuildTemplate } from "./templates/edit_guild_template.ts"; +import { getGuildTemplates } from "./templates/get_guild_templates.ts"; +import { getTemplate } from "./templates/get_template.ts"; +import { syncGuildTemplate } from "./templates/sync_guild_template.ts"; +import { createWebhook } from "./webhooks/create_webhook.ts"; +import { deleteWebhook } from "./webhooks/delete_webhook.ts"; +import { deleteWebhookMessage } from "./webhooks/delete_webhook_message.ts"; +import { deleteWebhookWithToken } from "./webhooks/delete_webhook_with_token.ts"; +import { editWebhook } from "./webhooks/edit_webhook.ts"; +import { editWebhookMessage } from "./webhooks/edit_webhook_message.ts"; +import { editWebhookWithToken } from "./webhooks/edit_webhook_with_token.ts"; +import { executeWebhook } from "./webhooks/execute_webhook.ts"; +import { getWebhook } from "./webhooks/get_webhook.ts"; +import { getWebhooks } from "./webhooks/get_webhooks.ts"; +import { getWebhookWithToken } from "./webhooks/get_webhook_with_token.ts"; + +export { + addReaction, + addReactions, + addRole, + avatarURL, + banMember, + categoryChildrenIDs, + channelOverwriteHasPermission, + createEmoji, + createGuild, + createGuildChannel, + createGuildFromTemplate, + createGuildTemplate, + createInvite, + createRole, + createSlashCommand, + createWebhook, + deleteChannel, + deleteChannelOverwrite, + deleteEmoji, + deleteGuildTemplate, + deleteIntegration, + deleteInvite, + deleteMessage, + deleteMessages, + deleteRole, + deleteServer, + deleteSlashCommand, + deleteSlashResponse, + deleteWebhook, + deleteWebhookMessage, + deleteWebhookWithToken, + disconnectMember, + editBotNickname, + editBotProfile, + editChannel, + editChannelOverwrite, + editEmoji, + editGuild, + editGuildTemplate, + editIntegration, + editMember, + editMessage, + editRole, + editSlashResponse, + editWebhook, + editWebhookMessage, + editWebhookWithToken, + editWidget, + emojiURL, + executeSlashCommand, + executeWebhook, + fetchMembers, + followChannel, + getAuditLogs, + getAvailableVoiceRegions, + getBan, + getBans, + getChannel, + getChannelInvites, + getChannels, + getChannelWebhooks, + getEmoji, + getEmojis, + getGatewayBot, + getGuild, + getGuildPreview, + getGuildTemplates, + getIntegrations, + getInvite, + getInvites, + getMember, + getMembers, + getMembersByQuery, + getMessage, + getMessages, + getPins, + getPruneCount, + getReactions, + getRoles, + getSlashCommand, + getSlashCommands, + getTemplate, + getUser, + getVanityURL, + getVoiceRegions, + getWebhook, + getWebhooks, + getWebhookWithToken, + getWidget, + getWidgetImageURL, + getWidgetSettings, + guildBannerURL, + guildIconURL, + guildSplashURL, + isChannelSynced, + kickMember, + leaveGuild, + moveMember, + pinMessage, + pruneMembers, + publishMessage, + rawAvatarURL, + removeAllReactions, + removeReaction, + removeReactionEmoji, + removeRole, + removeUserReaction, + sendDirectMessage, + sendMessage, + startTyping, + swapChannels, + swapRoles, + syncGuildTemplate, + syncIntegration, + unbanMember, + unpinMessage, + upsertSlashCommand, + upsertSlashCommands, +}; + +export let helpers = { + // channels + channelOverwriteHasPermission, + createGuildChannel, + deleteChannelOverwrite, + deleteChannel, + editChannelOverwrite, + editChannel, + followChannel, + getChannelWebhooks, + getChannel, + getChannels, + getPins, + isChannelSynced, + startTyping, + swapChannels, + // commands + createSlashCommand, + deleteSlashCommand, + deleteSlashResponse, + editSlashResponse, + executeSlashCommand, + getSlashCommand, + getSlashCommands, + upsertSlashCommand, + upsertSlashCommands, + // emojis + createEmoji, + deleteEmoji, + editEmoji, + getEmoji, + getEmojis, + // guilds + categoryChildrenIDs, + createGuild, + deleteServer, + editGuild, + editWidget, + emojiURL, + getAuditLogs, + getAvailableVoiceRegions, + getBan, + getBans, + getGuildPreview, + getGuild, + getPruneCount, + getVanityURL, + getVoiceRegions, + getWidgetImageURL, + getWidgetSettings, + getWidget, + guildBannerURL, + guildIconURL, + guildSplashURL, + leaveGuild, + // integrations + deleteIntegration, + editIntegration, + getIntegrations, + syncIntegration, + // invites + createInvite, + deleteInvite, + getChannelInvites, + getInvite, + getInvites, + // members + avatarURL, + banMember, + disconnectMember, + editBotNickname, + editBotProfile, + editMember, + fetchMembers, + getMember, + getMembersByQuery, + getMembers, + kickMember, + moveMember, + pruneMembers, + rawAvatarURL, + sendDirectMessage, + unbanMember, + // messages + addReaction, + addReactions, + deleteMessage, + deleteMessages, + editMessage, + getMessage, + getMessages, + getReactions, + pinMessage, + publishMessage, + removeAllReactions, + removeReactionEmoji, + removeReaction, + removeUserReaction, + sendMessage, + unpinMessage, + // misc + getGatewayBot, + getUser, + // roles + addRole, + createRole, + deleteRole, + editRole, + getRoles, + removeRole, + swapRoles, + // templates + createGuildFromTemplate, + createGuildTemplate, + deleteGuildTemplate, + editGuildTemplate, + getGuildTemplates, + getTemplate, + syncGuildTemplate, + // webhooks + createWebhook, + deleteWebhookMessage, + deleteWebhookWithToken, + deleteWebhook, + editWebhookMessage, + editWebhookWithToken, + editWebhook, + executeWebhook, + getWebhookWithToken, + getWebhook, + getWebhooks, +}; + +export type Helpers = typeof helpers; + +export function updateHelpers(newHelpers: Partial) { + helpers = { + ...helpers, + ...newHelpers, + }; +} diff --git a/src/helpers/roles/add_role.ts b/src/helpers/roles/add_role.ts new file mode 100644 index 000000000..e065bcde4 --- /dev/null +++ b/src/helpers/roles/add_role.ts @@ -0,0 +1,34 @@ +import { botID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + isHigherPosition, + requireBotGuildPermissions, +} from "../../util/permissions.ts"; + +/** Add a role to the member */ +export async function addRole( + guildID: string, + memberID: string, + roleID: string, + reason?: string, +) { + const isHigherRolePosition = await isHigherPosition( + guildID, + botID, + roleID, + ); + if (!isHigherRolePosition) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + } + + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); + + const result = await RequestManager.put( + endpoints.GUILD_MEMBER_ROLE(guildID, memberID, roleID), + { reason }, + ); + + return result; +} diff --git a/src/helpers/roles/create_role.ts b/src/helpers/roles/create_role.ts new file mode 100644 index 000000000..e4a98cb8f --- /dev/null +++ b/src/helpers/roles/create_role.ts @@ -0,0 +1,31 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { CreateRoleOptions, RoleData } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + calculateBits, + requireBotGuildPermissions, +} from "../../util/permissions.ts"; + +/** Create a new role for the guild. Requires the MANAGE_ROLES permission. */ +export async function createRole( + guildID: string, + options: CreateRoleOptions, + reason?: string, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); + + const result = await RequestManager.post(endpoints.GUILD_ROLES(guildID), { + ...options, + permissions: calculateBits(options?.permissions || []), + reason, + }); + + const roleData = result as RoleData; + const role = await structures.createRoleStruct(roleData); + const guild = await cacheHandlers.get("guilds", guildID); + guild?.roles.set(role.id, role); + + return role; +} diff --git a/src/helpers/roles/delete_role.ts b/src/helpers/roles/delete_role.ts new file mode 100644 index 000000000..fca6f21ed --- /dev/null +++ b/src/helpers/roles/delete_role.ts @@ -0,0 +1,12 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Delete a guild role. Requires the MANAGE_ROLES permission. */ +export async function deleteRole(guildID: string, id: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); + + const result = await RequestManager.delete(endpoints.GUILD_ROLE(guildID, id)); + + return result; +} diff --git a/src/helpers/roles/edit_role.ts b/src/helpers/roles/edit_role.ts new file mode 100644 index 000000000..f726fe161 --- /dev/null +++ b/src/helpers/roles/edit_role.ts @@ -0,0 +1,25 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { CreateRoleOptions } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + calculateBits, + requireBotGuildPermissions, +} from "../../util/permissions.ts"; + +/** Edit a guild role. Requires the MANAGE_ROLES permission. */ +export async function editRole( + guildID: string, + id: string, + options: CreateRoleOptions, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); + + const result = await RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), { + ...options, + permissions: options.permissions + ? calculateBits(options.permissions) + : undefined, + }); + + return result; +} diff --git a/src/helpers/roles/get_roles.ts b/src/helpers/roles/get_roles.ts new file mode 100644 index 000000000..509446ef1 --- /dev/null +++ b/src/helpers/roles/get_roles.ts @@ -0,0 +1,15 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Returns a list of role objects for the guild. + * + * ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your roles will be cached in your guild.** + */ +export async function getRoles(guildID: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); + + const result = await RequestManager.get(endpoints.GUILD_ROLES(guildID)); + + return result; +} diff --git a/src/helpers/roles/remove_role.ts b/src/helpers/roles/remove_role.ts new file mode 100644 index 000000000..f26dcc965 --- /dev/null +++ b/src/helpers/roles/remove_role.ts @@ -0,0 +1,36 @@ +import { botID } from "../../bot.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { Errors } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { + isHigherPosition, + requireBotGuildPermissions, +} from "../../util/permissions.ts"; + +/** Remove a role from the member */ +export async function removeRole( + guildID: string, + memberID: string, + roleID: string, + reason?: string, +) { + const isHigherRolePosition = await isHigherPosition( + guildID, + botID, + roleID, + ); + if ( + !isHigherRolePosition + ) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + } + + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); + + const result = await RequestManager.delete( + endpoints.GUILD_MEMBER_ROLE(guildID, memberID, roleID), + { reason }, + ); + + return result; +} diff --git a/src/helpers/roles/swap_roles.ts b/src/helpers/roles/swap_roles.ts new file mode 100644 index 000000000..deed5d142 --- /dev/null +++ b/src/helpers/roles/swap_roles.ts @@ -0,0 +1,16 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { PositionSwap } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Modify the positions of a set of role objects for the guild. Requires the MANAGE_ROLES permission. */ +export async function swapRoles(guildID: string, rolePositons: PositionSwap) { + await requireBotGuildPermissions(guildID, ["MANAGE_ROLES"]); + + const result = await RequestManager.patch( + endpoints.GUILD_ROLES(guildID), + rolePositons, + ); + + return result; +} diff --git a/src/helpers/templates/create_guild_from_template.ts b/src/helpers/templates/create_guild_from_template.ts new file mode 100644 index 000000000..36d65adb9 --- /dev/null +++ b/src/helpers/templates/create_guild_from_template.ts @@ -0,0 +1,34 @@ +import { cacheHandlers } from "../../cache.ts"; +import { RequestManager } from "../../rest/request_manager.ts"; +import { + CreateGuildFromTemplate, + CreateGuildPayload, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { urlToBase64 } from "../../util/utils.ts"; + +/** + * Create a new guild based on a template + * NOTE: This endpoint can be used only by bots in less than 10 guilds. + */ +export async function createGuildFromTemplate( + templateCode: string, + data: CreateGuildFromTemplate, +) { + if ((await cacheHandlers.size("guilds")) >= 10) { + throw new Error( + "This function can only be used by bots in less than 10 guilds.", + ); + } + + if (data.icon) { + data.icon = await urlToBase64(data.icon); + } + + const result = await await RequestManager.post( + endpoints.GUILD_TEMPLATE(templateCode), + data, + ); + + return result as CreateGuildPayload; +} diff --git a/src/helpers/templates/create_guild_template.ts b/src/helpers/templates/create_guild_template.ts new file mode 100644 index 000000000..38f755116 --- /dev/null +++ b/src/helpers/templates/create_guild_template.ts @@ -0,0 +1,33 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { CreateGuildTemplate, GuildTemplate } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** + * Creates a template for the guild. + * Requires the `MANAGE_GUILD` permission. + * @param name name of the template (1-100 characters) + * @param description description for the template (0-120 characters + */ +export async function createGuildTemplate( + guildID: string, + data: CreateGuildTemplate, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + if (data.name.length < 1 || data.name.length > 100) { + throw new Error("The name can only be in between 1-100 characters."); + } + + if (data.description?.length && data.description.length > 120) { + throw new Error("The description can only be in between 0-120 characters."); + } + + const template = (await RequestManager.post( + endpoints.GUILD_TEMPLATES(guildID), + data, + )) as GuildTemplate; + + return structures.createTemplateStruct(template); +} diff --git a/src/helpers/templates/delete_guild_template.ts b/src/helpers/templates/delete_guild_template.ts new file mode 100644 index 000000000..325019069 --- /dev/null +++ b/src/helpers/templates/delete_guild_template.ts @@ -0,0 +1,22 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { GuildTemplate } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** + * Deletes a template from a guild. + * Requires the `MANAGE_GUILD` permission. + */ +export async function deleteGuildTemplate( + guildID: string, + templateCode: string, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const deletedTemplate = (await RequestManager.delete( + `${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`, + )) as GuildTemplate; + + return structures.createTemplateStruct(deletedTemplate); +} diff --git a/src/helpers/templates/edit_guild_template.ts b/src/helpers/templates/edit_guild_template.ts new file mode 100644 index 000000000..1bae9cce6 --- /dev/null +++ b/src/helpers/templates/edit_guild_template.ts @@ -0,0 +1,32 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { EditGuildTemplate, GuildTemplate } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** + * Edit a template's metadata. + * Requires the `MANAGE_GUILD` permission. + */ +export async function editGuildTemplate( + guildID: string, + templateCode: string, + data: EditGuildTemplate, +) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + if (data.name?.length && (data.name.length < 1 || data.name.length > 100)) { + throw new Error("The name can only be in between 1-100 characters."); + } + + if (data.description?.length && data.description.length > 120) { + throw new Error("The description can only be in between 0-120 characters."); + } + + const template = (await RequestManager.patch( + `${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`, + data, + )) as GuildTemplate; + + return structures.createTemplateStruct(template); +} diff --git a/src/helpers/templates/get_guild_templates.ts b/src/helpers/templates/get_guild_templates.ts new file mode 100644 index 000000000..87558ece6 --- /dev/null +++ b/src/helpers/templates/get_guild_templates.ts @@ -0,0 +1,19 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { GuildTemplate } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** + * Returns an array of templates. + * Requires the `MANAGE_GUILD` permission. + */ +export async function getGuildTemplates(guildID: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const templates = (await RequestManager.get( + endpoints.GUILD_TEMPLATES(guildID), + )) as GuildTemplate[]; + + return templates.map((template) => structures.createTemplateStruct(template)); +} diff --git a/src/helpers/templates/get_template.ts b/src/helpers/templates/get_template.ts new file mode 100644 index 000000000..bc71ea484 --- /dev/null +++ b/src/helpers/templates/get_template.ts @@ -0,0 +1,14 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { GuildTemplate } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns the guild template if it exists */ +export async function getTemplate(templateCode: string) { + const result = (await RequestManager.get( + endpoints.GUILD_TEMPLATE(templateCode), + ) as GuildTemplate); + const template = await structures.createTemplateStruct(result); + + return template; +} diff --git a/src/helpers/templates/sync_guild_template.ts b/src/helpers/templates/sync_guild_template.ts new file mode 100644 index 000000000..d02861443 --- /dev/null +++ b/src/helpers/templates/sync_guild_template.ts @@ -0,0 +1,19 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { GuildTemplate } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** + * Syncs the template to the guild's current state. + * Requires the `MANAGE_GUILD` permission. + */ +export async function syncGuildTemplate(guildID: string, templateCode: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_GUILD"]); + + const template = (await RequestManager.put( + `${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`, + )) as GuildTemplate; + + return structures.createTemplateStruct(template); +} diff --git a/src/helpers/webhooks/create_webhook.ts b/src/helpers/webhooks/create_webhook.ts new file mode 100644 index 000000000..d259fc561 --- /dev/null +++ b/src/helpers/webhooks/create_webhook.ts @@ -0,0 +1,41 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { + Errors, + WebhookCreateOptions, + WebhookPayload, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; +import { urlToBase64 } from "../../util/utils.ts"; + +/** + * Create a new webhook. Requires the MANAGE_WEBHOOKS permission. Returns a webhook object on success. Webhook names follow our naming restrictions that can be found in our Usernames and Nicknames documentation, with the following additional stipulations: + * + * Webhook names cannot be: 'clyde' + */ +export async function createWebhook( + channelID: string, + options: WebhookCreateOptions, +) { + await requireBotChannelPermissions(channelID, ["MANAGE_WEBHOOKS"]); + + if ( + // Specific usernames that discord does not allow + options.name === "clyde" || + // Character limit checks. [...] checks are because of js unicode length handling + [...options.name].length < 2 || + [...options.name].length > 32 + ) { + throw new Error(Errors.INVALID_WEBHOOK_NAME); + } + + const result = await RequestManager.post( + endpoints.CHANNEL_WEBHOOKS(channelID), + { + ...options, + avatar: options.avatar ? await urlToBase64(options.avatar) : undefined, + }, + ); + + return result as WebhookPayload; +} diff --git a/src/helpers/webhooks/delete_webhook.ts b/src/helpers/webhooks/delete_webhook.ts new file mode 100644 index 000000000..2834aefe8 --- /dev/null +++ b/src/helpers/webhooks/delete_webhook.ts @@ -0,0 +1,12 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Delete a webhook permanently. Requires the `MANAGE_WEBHOOKS` permission. Returns a undefined on success */ +export async function deleteWebhook(channelID: string, webhookID: string) { + await requireBotChannelPermissions(channelID, ["MANAGE_WEBHOOKS"]); + + const result = await RequestManager.delete(endpoints.WEBHOOK_ID(webhookID)); + + return result; +} diff --git a/src/helpers/webhooks/delete_webhook_message.ts b/src/helpers/webhooks/delete_webhook_message.ts new file mode 100644 index 000000000..4741d2b79 --- /dev/null +++ b/src/helpers/webhooks/delete_webhook_message.ts @@ -0,0 +1,14 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +export async function deleteWebhookMessage( + webhookID: string, + webhookToken: string, + messageID: string, +) { + const result = await RequestManager.delete( + endpoints.WEBHOOK_MESSAGE(webhookID, webhookToken, messageID), + ); + + return result; +} diff --git a/src/helpers/webhooks/delete_webhook_with_token.ts b/src/helpers/webhooks/delete_webhook_with_token.ts new file mode 100644 index 000000000..5230f7eb7 --- /dev/null +++ b/src/helpers/webhooks/delete_webhook_with_token.ts @@ -0,0 +1,14 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Delete a webhook permanently. Returns a undefined on success */ +export async function deleteWebhookWithToken( + webhookID: string, + webhookToken: string, +) { + const result = await RequestManager.delete( + endpoints.WEBHOOK(webhookID, webhookToken), + ); + + return result; +} diff --git a/src/helpers/webhooks/edit_webhook.ts b/src/helpers/webhooks/edit_webhook.ts new file mode 100644 index 000000000..d2c202ae5 --- /dev/null +++ b/src/helpers/webhooks/edit_webhook.ts @@ -0,0 +1,20 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { WebhookEditOptions, WebhookPayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; + +/** Edit a webhook. Requires the `MANAGE_WEBHOOKS` permission. Returns the updated webhook object on success. */ +export async function editWebhook( + channelID: string, + webhookID: string, + options: WebhookEditOptions, +) { + await requireBotChannelPermissions(channelID, ["MANAGE_WEBHOOKS"]); + + const result = await RequestManager.patch(endpoints.WEBHOOK_ID(webhookID), { + ...options, + channel_id: options.channelID, + }); + + return result as WebhookPayload; +} diff --git a/src/helpers/webhooks/edit_webhook_message.ts b/src/helpers/webhooks/edit_webhook_message.ts new file mode 100644 index 000000000..37079a25f --- /dev/null +++ b/src/helpers/webhooks/edit_webhook_message.ts @@ -0,0 +1,63 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { + EditWebhookMessageOptions, + Errors, + MessageCreateOptions, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +export async function editWebhookMessage( + webhookID: string, + webhookToken: string, + messageID: string, + options: EditWebhookMessageOptions, +) { + if (options.content && options.content.length > 2000) { + throw Error(Errors.MESSAGE_MAX_LENGTH); + } + + if (options.embeds && options.embeds.length > 10) { + options.embeds.splice(10); + } + + if (options.allowed_mentions) { + if (options.allowed_mentions.users?.length) { + if (options.allowed_mentions.parse.includes("users")) { + options.allowed_mentions.parse = options.allowed_mentions.parse.filter( + (p) => p !== "users", + ); + } + + if (options.allowed_mentions.users.length > 100) { + options.allowed_mentions.users = options.allowed_mentions.users.slice( + 0, + 100, + ); + } + } + + if (options.allowed_mentions.roles?.length) { + if (options.allowed_mentions.parse.includes("roles")) { + options.allowed_mentions.parse = options.allowed_mentions.parse.filter( + (p) => p !== "roles", + ); + } + + if (options.allowed_mentions.roles.length > 100) { + options.allowed_mentions.roles = options.allowed_mentions.roles.slice( + 0, + 100, + ); + } + } + } + + const result = await RequestManager.patch( + endpoints.WEBHOOK_MESSAGE(webhookID, webhookToken, messageID), + { ...options, allowed_mentions: options.allowed_mentions }, + ) as MessageCreateOptions; + + const message = await structures.createMessageStruct(result); + return message; +} diff --git a/src/helpers/webhooks/edit_webhook_with_token.ts b/src/helpers/webhooks/edit_webhook_with_token.ts new file mode 100644 index 000000000..617fe8eb4 --- /dev/null +++ b/src/helpers/webhooks/edit_webhook_with_token.ts @@ -0,0 +1,17 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { WebhookEditOptions, WebhookPayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Edit a webhook. Returns the updated webhook object on success. */ +export async function editWebhookWithToken( + webhookID: string, + webhookToken: string, + options: Omit, +) { + const result = await RequestManager.patch( + endpoints.WEBHOOK(webhookID, webhookToken), + options, + ); + + return result as WebhookPayload; +} diff --git a/src/helpers/webhooks/execute_webhook.ts b/src/helpers/webhooks/execute_webhook.ts new file mode 100644 index 000000000..8a0024104 --- /dev/null +++ b/src/helpers/webhooks/execute_webhook.ts @@ -0,0 +1,67 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { structures } from "../../structures/mod.ts"; +import { + Errors, + ExecuteWebhookOptions, + MessageCreateOptions, +} from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Execute a webhook with webhook ID and webhook token */ +export async function executeWebhook( + webhookID: string, + webhookToken: string, + options: ExecuteWebhookOptions, +) { + if (!options.content && !options.file && !options.embeds) { + throw new Error(Errors.INVALID_WEBHOOK_OPTIONS); + } + + if (options.content && options.content.length > 2000) { + throw Error(Errors.MESSAGE_MAX_LENGTH); + } + + if (options.embeds && options.embeds.length > 10) { + options.embeds.splice(10); + } + + if (options.mentions) { + if (options.mentions.users?.length) { + if (options.mentions.parse.includes("users")) { + options.mentions.parse = options.mentions.parse.filter( + (p) => p !== "users", + ); + } + + if (options.mentions.users.length > 100) { + options.mentions.users = options.mentions.users.slice(0, 100); + } + } + + if (options.mentions.roles?.length) { + if (options.mentions.parse.includes("roles")) { + options.mentions.parse = options.mentions.parse.filter( + (p) => p !== "roles", + ); + } + + if (options.mentions.roles.length > 100) { + options.mentions.roles = options.mentions.roles.slice(0, 100); + } + } + } + + const result = await RequestManager.post( + `${endpoints.WEBHOOK(webhookID, webhookToken)}${ + options.wait ? "?wait=true" : "" + }`, + { + ...options, + allowed_mentions: options.mentions, + avatar_url: options.avatar_url, + }, + ); + if (!options.wait) return; + + return structures.createMessageStruct(result as MessageCreateOptions); +} diff --git a/src/helpers/webhooks/get_webhook.ts b/src/helpers/webhooks/get_webhook.ts new file mode 100644 index 000000000..3543b5a8b --- /dev/null +++ b/src/helpers/webhooks/get_webhook.ts @@ -0,0 +1,10 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { WebhookPayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns the new webhook object for the given id. */ +export async function getWebhook(webhookID: string) { + const result = await RequestManager.get(endpoints.WEBHOOK_ID(webhookID)); + + return result as WebhookPayload; +} diff --git a/src/helpers/webhooks/get_webhook_with_token.ts b/src/helpers/webhooks/get_webhook_with_token.ts new file mode 100644 index 000000000..b9a173758 --- /dev/null +++ b/src/helpers/webhooks/get_webhook_with_token.ts @@ -0,0 +1,10 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { WebhookPayload } from "../../types/mod.ts"; +import { endpoints } from "../../util/constants.ts"; + +/** Returns the new webhook object for the given id, this call does not require authentication and returns no user in the webhook object. */ +export async function getWebhookWithToken(webhookID: string, token: string) { + const result = await RequestManager.get(endpoints.WEBHOOK(webhookID, token)); + + return result as WebhookPayload; +} diff --git a/src/helpers/webhooks/get_webhooks.ts b/src/helpers/webhooks/get_webhooks.ts new file mode 100644 index 000000000..680921958 --- /dev/null +++ b/src/helpers/webhooks/get_webhooks.ts @@ -0,0 +1,12 @@ +import { RequestManager } from "../../rest/request_manager.ts"; +import { endpoints } from "../../util/constants.ts"; +import { requireBotGuildPermissions } from "../../util/permissions.ts"; + +/** Returns a list of guild webhooks objects. Requires the MANAGE_WEBHOOKs permission. */ +export async function getWebhooks(guildID: string) { + await requireBotGuildPermissions(guildID, ["MANAGE_WEBHOOKS"]); + + const result = await RequestManager.get(endpoints.GUILD_WEBHOOKS(guildID)); + + return result; +} diff --git a/src/interactions/deps.ts b/src/interactions/deps.ts index 0a4ff58ff..02799f769 100644 --- a/src/interactions/deps.ts +++ b/src/interactions/deps.ts @@ -1,2 +1,2 @@ -export { serve } from "https://deno.land/std@0.88.0/http/server.ts"; +export { serve } from "https://deno.land/std@0.90.0/http/server.ts"; export { verify } from "https://esm.sh/@evan/wasm@0.0.49/target/ed25519/deno.js"; diff --git a/src/interactions/server.ts b/src/interactions/server.ts index 9b20a517b..822b67587 100644 --- a/src/interactions/server.ts +++ b/src/interactions/server.ts @@ -12,8 +12,8 @@ const serverOptions = { port: 80, }; -/** Theses are the controllers that you can plug into and customize to your needs. */ -export const controllers = { +/** Theses are the handlers that you can plug into and customize to your needs. */ +export const handlers = { handlePayload, handleApplicationCommand, }; @@ -36,7 +36,7 @@ export async function startServer( serverOptions.publicKey = publicKey; serverOptions.port = port; if (handleApplicationCommand) { - controllers.handleApplicationCommand = handleApplicationCommand; + handlers.handleApplicationCommand = handleApplicationCommand; } const server = serve({ port: serverOptions.port }); @@ -59,7 +59,7 @@ export async function startServer( try { const data = JSON.parse(new TextDecoder().decode(buffer)); - const response = await controllers.handlePayload(data); + const response = await handlers.handlePayload(data); req.respond( { status: response.status || 200, body: JSON.stringify(response.body) }, ); @@ -74,7 +74,7 @@ function handlePayload(payload: Interaction) { case InteractionType.PING: return { status: 200, body: { type: InteractionResponseType.PONG } }; default: // APPLICATION_COMMAND - return controllers.handleApplicationCommand(payload); + return handlers.handleApplicationCommand(payload); } } diff --git a/src/interactions/types/member.ts b/src/interactions/types/member.ts index 94634a62e..a0bb175c5 100644 --- a/src/interactions/types/member.ts +++ b/src/interactions/types/member.ts @@ -40,4 +40,6 @@ export interface MemberCreatePayload { deaf: boolean; /** Whether the user is muted in voice channels */ mute: boolean; + /** total permissions of the member in the channel, including overrides */ + permissions: string; } diff --git a/src/interactions/types/webhook.ts b/src/interactions/types/webhook.ts index 9743bb8b9..0a65112a6 100644 --- a/src/interactions/types/webhook.ts +++ b/src/interactions/types/webhook.ts @@ -12,7 +12,7 @@ export interface ExecuteWebhookOptions { /** true if this is a TTS message */ tts?: boolean; /** file contents the contents of the file being sent one of content, file, embeds */ - file?: { blob: unknown; name: string }; + file?: { blob: Blob; name: string }; /** array of up to 10 embed objects embedded rich content. */ embeds?: Embed[]; /** allowed mentions for the message */ diff --git a/src/rest/deps.ts b/src/rest/deps.ts index 53b351893..b02feea5b 100644 --- a/src/rest/deps.ts +++ b/src/rest/deps.ts @@ -1 +1 @@ -export * from "https://deno.land/std@0.88.0/http/server.ts"; +export * from "https://deno.land/std@0.90.0/http/server.ts"; diff --git a/src/rest/request_manager.ts b/src/rest/request_manager.ts index af77cb559..02e12b6a9 100644 --- a/src/rest/request_manager.ts +++ b/src/rest/request_manager.ts @@ -166,7 +166,7 @@ function createRequestBody(body: any, method: RequestMethods) { body.file.map((file: FileContent, index: number) => // The key of the form data item must be unique; otherwise, Discordeno only considers the first item in the form data with the same names - form.append(`file${index + 1}`, file.blob as Blob, file.name) + form.append(`file${index + 1}`, file.blob, file.name) ); form.append("payload_json", JSON.stringify({ ...body, file: undefined })); diff --git a/src/api/structures/channel.ts b/src/structures/channel.ts similarity index 87% rename from src/api/structures/channel.ts rename to src/structures/channel.ts index f0706c3ab..276519c76 100644 --- a/src/api/structures/channel.ts +++ b/src/structures/channel.ts @@ -1,3 +1,10 @@ +import { channelOverwriteHasPermission } from "../helpers/channels/channel_overwrite_has_permission.ts"; +import { deleteChannel } from "../helpers/channels/delete_channel.ts"; +import { deleteChannelOverwrite } from "../helpers/channels/delete_channel_overwrite.ts"; +import { editChannel } from "../helpers/channels/edit_channel.ts"; +import { editChannelOverwrite } from "../helpers/channels/edit_channel_overwrite.ts"; +import { kickFromVoiceChannel } from "../helpers/members/disconnect_member.ts"; +import { sendMessage } from "../helpers/messages/send_message.ts"; import { ChannelCreatePayload, ChannelEditOptions, @@ -6,21 +13,10 @@ import { Overwrite, Permission, RawOverwrite, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { Collection } from "../../util/collection.ts"; -import { createNewProp } from "../../util/utils.ts"; -import { - channelOverwriteHasPermission, - editChannel, - sendMessage, -} from "../handlers/channel.ts"; -import { - deleteChannel, - deleteChannelOverwrite, - editChannelOverwrite, -} from "../handlers/guild.ts"; -import { kickFromVoiceChannel } from "../handlers/member.ts"; +} from "../types/mod.ts"; +import { cache } from "../util/cache.ts"; +import { Collection } from "../util/collection.ts"; +import { createNewProp } from "../util/utils.ts"; import { CleanVoiceState, Guild } from "./guild.ts"; import { Member } from "./member.ts"; import { Message } from "./message.ts"; @@ -77,7 +73,7 @@ const baseChannel: Partial = { }; // deno-lint-ignore require-await -export async function createChannel( +export async function createChannelStruct( data: ChannelCreatePayload, guildID?: string, ) { @@ -135,7 +131,7 @@ export interface Channel { lastMessageID?: string; /** The amount of users allowed in this voice channel. */ userLimit?: number; - /** The rate limit(slowmode) in this text channel that users can send messages. */ + /** The rate limit (slowmode) in this text channel that users can send messages. */ rateLimitPerUser?: number; /** The category id for this channel */ parentID?: string; diff --git a/src/api/structures/guild.ts b/src/structures/guild.ts similarity index 88% rename from src/api/structures/guild.ts rename to src/structures/guild.ts index f1cc711cb..1d6c7b4ca 100644 --- a/src/api/structures/guild.ts +++ b/src/structures/guild.ts @@ -1,4 +1,16 @@ -import { botID } from "../../bot.ts"; +import { botID } from "../bot.ts"; +import { cacheHandlers } from "../cache.ts"; +import { deleteServer } from "../helpers/guilds/delete_server.ts"; +import { editGuild } from "../helpers/guilds/edit_guild.ts"; +import { getAuditLogs } from "../helpers/guilds/get_audit_logs.ts"; +import { getBan } from "../helpers/guilds/get_ban.ts"; +import { getBans } from "../helpers/guilds/get_bans.ts"; +import { guildBannerURL } from "../helpers/guilds/guild_banner_url.ts"; +import { guildIconURL } from "../helpers/guilds/guild_icon_url.ts"; +import { leaveGuild } from "../helpers/guilds/leave_guild.ts"; +import { getInvites } from "../helpers/invites/get_invites.ts"; +import { banMember } from "../helpers/members/ban_member.ts"; +import { unbanMember } from "../helpers/members/unban_member.ts"; import { BanOptions, CreateGuildPayload, @@ -12,24 +24,10 @@ import { MemberCreatePayload, Presence, VoiceState, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { Collection } from "../../util/collection.ts"; -import { createNewProp } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; -import { - ban, - deleteServer, - editGuild, - getAuditLogs, - getBan, - getBans, - getInvites, - guildBannerURL, - guildIconURL, - leaveGuild, - unban, -} from "../handlers/guild.ts"; +} from "../types/mod.ts"; +import { cache } from "../util/cache.ts"; +import { Collection } from "../util/collection.ts"; +import { createNewProp } from "../util/utils.ts"; import { Member } from "./member.ts"; import { Channel, Role, structures } from "./mod.ts"; @@ -91,10 +89,10 @@ const baseGuild: Partial = { return getBans(this.id!); }, ban(memberID, options) { - return ban(this.id!, memberID, options); + return banMember(this.id!, memberID, options); }, unban(memberID) { - return unban(this.id!, memberID); + return unbanMember(this.id!, memberID); }, invites() { return getInvites(this.id!); @@ -107,7 +105,10 @@ const baseGuild: Partial = { }, }; -export async function createGuild(data: CreateGuildPayload, shardID: number) { +export async function createGuildStruct( + data: CreateGuildPayload, + shardID: number, +) { const { disovery_splash: discoverySplash, default_message_notifications: defaultMessageNotifications, @@ -138,15 +139,19 @@ export async function createGuild(data: CreateGuildPayload, shardID: number) { channels = [], members, presences = [], + emojis, ...rest } = data; const roles = await Promise.all( - data.roles.map((role) => structures.createRole(role)), + data.roles.map((role) => structures.createRoleStruct(role)), ); await Promise.all(channels.map(async (channel) => { - const channelStruct = await structures.createChannel(channel, rest.id); + const channelStruct = await structures.createChannelStruct( + channel, + rest.id, + ); return cacheHandlers.set("channels", channelStruct.id, channelStruct); })); @@ -187,6 +192,9 @@ export async function createGuild(data: CreateGuildPayload, shardID: number) { new Collection(presences.map((p: Presence) => [p.user.id, p])), ), memberCount: createNewProp(memberCount), + emojis: createNewProp( + new Collection(emojis.map((emoji) => [emoji.id ?? emoji.name, emoji])), + ), voiceStates: createNewProp( new Collection( voiceStates.map((vs: VoiceState) => [ @@ -229,7 +237,7 @@ export interface Guild { /** Explicit content filter level */ explicitContentFilter: number; /** The custom guild emojis */ - emojis: Emoji[]; + emojis: Collection; /** Enabled guild features */ features: GuildFeatures[]; /** System channel flags */ @@ -338,9 +346,9 @@ export interface Guild { /** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */ bans(): ReturnType; /** Ban a user from the guild and optionally delete previous messages sent by the user. Requires the BAN_MEMBERS permission. */ - ban(memberID: string, options: BanOptions): ReturnType; + ban(memberID: string, options: BanOptions): ReturnType; /** Remove the ban for a user. Requires BAN_MEMBERS permission */ - unban(memberID: string): ReturnType; + unban(memberID: string): ReturnType; /** Get all the invites for this guild. Requires MANAGE_GUILD permission */ invites(): ReturnType; } diff --git a/src/api/structures/member.ts b/src/structures/member.ts similarity index 84% rename from src/api/structures/member.ts rename to src/structures/member.ts index 3b385172f..2b2da411e 100644 --- a/src/api/structures/member.ts +++ b/src/structures/member.ts @@ -1,3 +1,11 @@ +import { cacheHandlers } from "../cache.ts"; +import { banMember } from "../helpers/members/ban_member.ts"; +import { editMember } from "../helpers/members/edit_member.ts"; +import { kickMember } from "../helpers/members/kick_member.ts"; +import { rawAvatarURL } from "../helpers/members/raw_avatar_url.ts"; +import { sendDirectMessage } from "../helpers/members/send_direct_message.ts"; +import { addRole } from "../helpers/roles/add_role.ts"; +import { removeRole } from "../helpers/roles/remove_role.ts"; import { BanOptions, EditMemberOptions, @@ -6,20 +14,10 @@ import { ImageSize, MemberCreatePayload, MessageContent, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { Collection } from "../../util/collection.ts"; -import { createNewProp } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; -import { ban } from "../handlers/guild.ts"; -import { - addRole, - editMember, - kick, - rawAvatarURL, - removeRole, - sendDirectMessage, -} from "../handlers/member.ts"; +} from "../types/mod.ts"; +import { cache } from "../util/cache.ts"; +import { Collection } from "../util/collection.ts"; +import { createNewProp } from "../util/utils.ts"; import { Guild } from "./guild.ts"; const baseMember: Partial = { @@ -56,13 +54,13 @@ const baseMember: Partial = { return sendDirectMessage(this.id!, content); }, kick(guildID, reason) { - return kick(guildID, this.id!, reason); + return kickMember(guildID, this.id!, reason); }, edit(guildID, options) { return editMember(guildID, this.id!, options); }, ban(guildID, options) { - return ban(guildID, this.id!, options); + return banMember(guildID, this.id!, options); }, addRole(guildID, roleID, reason) { return addRole(guildID, this.id!, roleID, reason); @@ -72,7 +70,10 @@ const baseMember: Partial = { }, }; -export async function createMember(data: MemberCreatePayload, guildID: string) { +export async function createMemberStruct( + data: MemberCreatePayload, + guildID: string, +) { const { joined_at: joinedAt, premium_since: premiumSince, @@ -178,14 +179,14 @@ export interface Member { content: string | MessageContent, ): ReturnType; /** Kick the member from a guild */ - kick(guildID: string, reason?: string): ReturnType; + kick(guildID: string, reason?: string): ReturnType; /** Edit the member in a guild */ edit( guildID: string, options: EditMemberOptions, ): ReturnType; /** Ban a member in a guild */ - ban(guildID: string, options: BanOptions): ReturnType; + ban(guildID: string, options: BanOptions): ReturnType; /** Add a role to the member */ addRole( guildID: string, diff --git a/src/api/structures/message.ts b/src/structures/message.ts similarity index 86% rename from src/api/structures/message.ts rename to src/structures/message.ts index 47e828cae..9b8b2c203 100644 --- a/src/api/structures/message.ts +++ b/src/structures/message.ts @@ -1,31 +1,29 @@ +import { cacheHandlers } from "../cache.ts"; +import { sendDirectMessage } from "../helpers/members/send_direct_message.ts"; +import { addReaction } from "../helpers/messages/add_reaction.ts"; +import { addReactions } from "../helpers/messages/add_reactions.ts"; +import { deleteMessage } from "../helpers/messages/delete_message.ts"; +import { editMessage } from "../helpers/messages/edit_message.ts"; +import { pinMessage } from "../helpers/messages/pin_message.ts"; +import { removeAllReactions } from "../helpers/messages/remove_all_reactions.ts"; +import { removeReaction } from "../helpers/messages/remove_reaction.ts"; +import { removeReactionEmoji } from "../helpers/messages/remove_reaction_emoji.ts"; +import { sendMessage } from "../helpers/messages/send_message.ts"; import { Activity, Application, Attachment, + DiscordReferencePayload, Embed, GuildMember, MessageContent, MessageCreateOptions, MessageSticker, Reaction, - Reference, UserPayload, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { createNewProp } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; -import { sendMessage } from "../handlers/channel.ts"; -import { sendDirectMessage } from "../handlers/member.ts"; -import { - addReaction, - addReactions, - deleteMessageByID, - editMessage, - pin, - removeAllReactions, - removeReaction, - removeReactionEmoji, -} from "../handlers/message.ts"; +} from "../types/mod.ts"; +import { cache } from "../util/cache.ts"; +import { createNewProp } from "../util/utils.ts"; import { Channel } from "./channel.ts"; import { Guild } from "./guild.ts"; import { Member } from "./member.ts"; @@ -64,7 +62,7 @@ const baseMessage: Partial = { // METHODS delete(reason, delayMilliseconds) { - return deleteMessageByID( + return deleteMessage( this.channelID!, this.id!, reason, @@ -75,7 +73,7 @@ const baseMessage: Partial = { return editMessage(this as Message, content); }, pin() { - return pin(this.channelID!, this.id!); + return pinMessage(this.channelID!, this.id!); }, addReaction(reaction) { return addReaction(this.channelID!, this.id!, reaction); @@ -85,11 +83,17 @@ const baseMessage: Partial = { }, reply(content) { const contentWithMention = typeof content === "string" - ? { content, mentions: { repliedUser: true }, replyMessageID: this.id } + ? { + content, + mentions: { repliedUser: true }, + replyMessageID: this.id, + failReplyIfNotExists: false, + } : { ...content, mentions: { ...(content.mentions || {}), repliedUser: true }, replyMessageID: this.id, + failReplyIfNotExists: content.failReplyIfNotExists === true, }; if (this.guildID) return sendMessage(this.channelID!, contentWithMention); @@ -126,7 +130,7 @@ const baseMessage: Partial = { }, }; -export async function createMessage(data: MessageCreateOptions) { +export async function createMessageStruct(data: MessageCreateOptions) { const { guild_id: guildID = "", channel_id: channelID, @@ -216,7 +220,7 @@ export interface Message { /** Applications that sent with Rich Presence related chat embeds. */ applications?: Application; /** The reference data sent with crossposted messages */ - messageReference?: Reference; + messageReference?: DiscordReferencePayload; /** The message flags combined like permission bits describe extra features of the message */ flags?: 1 | 2 | 4 | 8 | 16; /** the stickers sent with the message (bots currently can only receive messages with stickers, not send) */ @@ -249,11 +253,11 @@ export interface Message { delete( reason?: string, delayMilliseconds?: number, - ): ReturnType; + ): ReturnType; /** Edit the message */ edit(content: string | MessageContent): ReturnType; /** Pins the message in the channel */ - pin(): ReturnType; + pin(): ReturnType; /** Add a reaction to the message */ addReaction(reaction: string): ReturnType; /** Add multiple reactions to the message without or without order. */ diff --git a/src/structures/mod.ts b/src/structures/mod.ts new file mode 100644 index 000000000..8c3e44724 --- /dev/null +++ b/src/structures/mod.ts @@ -0,0 +1,32 @@ +import { Channel, createChannelStruct } from "./channel.ts"; +import { createGuildStruct, Guild } from "./guild.ts"; +import { createMemberStruct, Member } from "./member.ts"; +import { createMessageStruct, Message } from "./message.ts"; +import { createRoleStruct, Role } from "./role.ts"; +import { createTemplateStruct, Template } from "./template.ts"; + +/** This is the placeholder where the structure creation functions are kept. */ +export let structures = { + createChannelStruct, + createGuildStruct, + createMemberStruct, + createMessageStruct, + createRoleStruct, + createTemplateStruct, +}; + +export type { Channel, Guild, Member, Message, Role, Template }; + +export type Structures = typeof structures; + +/** This function is used to update/reload/customize the internal structures of Discordeno. + * + * ⚠️ **ADVANCED USE ONLY: If you customize this incorrectly, you could potentially create many new errors/bugs. + * Please take caution when using this.** +*/ +export function updateStructures(newStructures: Structures) { + structures = { + ...structures, + ...newStructures, + }; +} diff --git a/src/api/structures/role.ts b/src/structures/role.ts similarity index 90% rename from src/api/structures/role.ts rename to src/structures/role.ts index ca09e60fd..efad15416 100644 --- a/src/api/structures/role.ts +++ b/src/structures/role.ts @@ -1,8 +1,9 @@ -import { CreateRoleOptions, RoleData } from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { Collection } from "../../util/collection.ts"; -import { createNewProp } from "../../util/utils.ts"; -import { deleteRole, editRole } from "../handlers/guild.ts"; +import { deleteRole } from "../helpers/roles/delete_role.ts"; +import { editRole } from "../helpers/roles/edit_role.ts"; +import { CreateRoleOptions, RoleData } from "../types/mod.ts"; +import { cache } from "../util/cache.ts"; +import { Collection } from "../util/collection.ts"; +import { createNewProp } from "../util/utils.ts"; import { Guild } from "./guild.ts"; import { Member } from "./member.ts"; @@ -67,7 +68,7 @@ const baseRole: Partial = { }; // deno-lint-ignore require-await -export async function createRole({ tags = {}, ...rest }: RoleData) { +export async function createRoleStruct({ tags = {}, ...rest }: RoleData) { const restProps: Record> = {}; for (const key of Object.keys(rest)) { // @ts-ignore index signature diff --git a/src/api/structures/template.ts b/src/structures/template.ts similarity index 90% rename from src/api/structures/template.ts rename to src/structures/template.ts index 77e574651..dafb1e5e7 100644 --- a/src/api/structures/template.ts +++ b/src/structures/template.ts @@ -1,6 +1,6 @@ -import { GuildTemplate, UserPayload } from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { createNewProp } from "../../util/utils.ts"; +import { GuildTemplate, UserPayload } from "../types/mod.ts"; +import { cache } from "../util/cache.ts"; +import { createNewProp } from "../util/utils.ts"; import { Guild } from "./guild.ts"; const baseTemplate: Partial