mirror of
https://github.com/discordeno/discordeno.git
synced 2026-06-16 19:28:17 +00:00
Merge branch 'next' of https://github.com/discordeno/discordeno into getters
This commit is contained in:
@@ -1,15 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/vscode/devcontainers/base:debian-10
|
|
||||||
|
|
||||||
ENV DENO_INSTALL=/deno
|
|
||||||
RUN mkdir -p /deno \
|
|
||||||
&& curl -fsSL https://deno.land/x/install/install.sh | sh \
|
|
||||||
&& chown -R vscode /deno
|
|
||||||
|
|
||||||
ENV PATH=${DENO_INSTALL}/bin:${PATH} \
|
|
||||||
DENO_DIR=${DENO_INSTALL}/.cache/deno
|
|
||||||
|
|
||||||
RUN deno cache deps.ts
|
|
||||||
|
|
||||||
# [Optional] Uncomment this section to install additional OS packages.
|
|
||||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Deno",
|
"name": "Deno",
|
||||||
"dockerFile": "Dockerfile",
|
"image": "hayd/debian-deno",
|
||||||
|
|
||||||
// Set *default* container specific settings.json values on container create.
|
// Set *default* container specific settings.json values on container create.
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
Don't worry a lot of developers start out coding their first projects as a Discord bot (I did 😉) and it is not so easy to do so. Discordeno is built considering all the issues with pre-existing libraries and issues that I had when I first started out coding bots.
|
Don't worry a lot of developers start out coding their first projects as a Discord bot (I did 😉) and it is not so easy to do so. Discordeno is built considering all the issues with pre-existing libraries and issues that I had when I first started out coding bots.
|
||||||
If you are a beginner developer, you may check out these awesome official and unofficial boilerplates:
|
If you are a beginner developer, you may check out these awesome official and unofficial boilerplates:
|
||||||
|
|
||||||
- [Official Discordeno Boilerplate](https://github.com/Skillz4Killz/Discordeno-bot-template)
|
- Official Discordeno Boilerplate
|
||||||
- [Dencord Starter](https://github.com/ayntee/dencord-starter)
|
- [GitHub](https://github.com/Skillz4Killz/Discordeno-bot-template)
|
||||||
- [Add your Own]
|
- [Features](https://github.com/Skillz4Killz/Discordeno-bot-template#features)
|
||||||
|
|
||||||
If you do not wish to use a boilerplate, you may continue reading.
|
If you do not wish to use a boilerplate, you may continue reading.
|
||||||
|
|
||||||
@@ -28,16 +28,22 @@ If you do not wish to use a boilerplate, you may continue reading.
|
|||||||
Here's a minimal example to get started with:
|
Here's a minimal example to get started with:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import startBot, { sendMessage, Intents } from "https://deno.land/x/discordeno@9.4.0/mod.ts";
|
import {
|
||||||
|
Intents,
|
||||||
|
sendMessage,
|
||||||
|
startBot,
|
||||||
|
} from "https://deno.land/x/discordeno@9.4.0/mod.ts";
|
||||||
|
|
||||||
startBot({
|
startBot({
|
||||||
token: "BOT TOKEN",
|
token: "BOT TOKEN",
|
||||||
intents: [Intents.GUILD_MESSAGES, Intents.GUILDS],
|
intents: [Intents.GUILD_MESSAGES, Intents.GUILDS],
|
||||||
eventHandlers: {
|
eventHandlers: {
|
||||||
ready: () => console.log('Successfully connected to gateway'),
|
ready() {
|
||||||
messageCreate: (message) => {
|
console.log("Successfully connected to gateway");
|
||||||
if (message.content === "hello") {
|
},
|
||||||
sendMessage(message.channelID, "Hi there!");
|
messageCreate(message) {
|
||||||
|
if (message.content === "ping") {
|
||||||
|
sendMessage(message.channelID, "Pong using Discordeno!");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,15 +13,14 @@ export * from "./src/api/handlers/guild.ts";
|
|||||||
export * from "./src/api/handlers/member.ts";
|
export * from "./src/api/handlers/member.ts";
|
||||||
export * from "./src/api/handlers/message.ts";
|
export * from "./src/api/handlers/message.ts";
|
||||||
export * from "./src/api/handlers/webhook.ts";
|
export * from "./src/api/handlers/webhook.ts";
|
||||||
export * from "./src/bot.ts";
|
|
||||||
export * from "./src/rest/mod.ts";
|
|
||||||
export * from "./src/ws/mod.ts";
|
|
||||||
export * from "./src/api/structures/channel.ts";
|
export * from "./src/api/structures/channel.ts";
|
||||||
export * from "./src/api/structures/guild.ts";
|
export * from "./src/api/structures/guild.ts";
|
||||||
export * from "./src/api/structures/member.ts";
|
export * from "./src/api/structures/member.ts";
|
||||||
export * from "./src/api/structures/message.ts";
|
export * from "./src/api/structures/message.ts";
|
||||||
export * from "./src/api/structures/mod.ts";
|
export * from "./src/api/structures/mod.ts";
|
||||||
export * from "./src/api/structures/role.ts";
|
export * from "./src/api/structures/role.ts";
|
||||||
|
export * from "./src/bot.ts";
|
||||||
|
export * from "./src/rest/mod.ts";
|
||||||
export * from "./src/types/activity.ts";
|
export * from "./src/types/activity.ts";
|
||||||
export * from "./src/types/cdn.ts";
|
export * from "./src/types/cdn.ts";
|
||||||
export * from "./src/types/channel.ts";
|
export * from "./src/types/channel.ts";
|
||||||
@@ -36,7 +35,7 @@ export * from "./src/types/permission.ts";
|
|||||||
export * from "./src/types/presence.ts";
|
export * from "./src/types/presence.ts";
|
||||||
export * from "./src/types/role.ts";
|
export * from "./src/types/role.ts";
|
||||||
export * from "./src/util/cache.ts";
|
export * from "./src/util/cache.ts";
|
||||||
export * from "./src/util/cdn.ts";
|
|
||||||
export * from "./src/util/collection.ts";
|
export * from "./src/util/collection.ts";
|
||||||
export * from "./src/util/permissions.ts";
|
export * from "./src/util/permissions.ts";
|
||||||
export * from "./src/util/utils.ts";
|
export * from "./src/util/utils.ts";
|
||||||
|
export * from "./src/ws/mod.ts";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Channel, Guild, Member, Message } from "../structures/structures.ts";
|
|
||||||
import { PresenceUpdatePayload } from "../../types/types.ts";
|
import { PresenceUpdatePayload } from "../../types/types.ts";
|
||||||
import { cache } from "../../util/cache.ts";
|
import { cache } from "../../util/cache.ts";
|
||||||
import { Collection } from "../../util/collection.ts";
|
import { Collection } from "../../util/collection.ts";
|
||||||
|
import { Channel, Guild, Member, Message } from "../structures/structures.ts";
|
||||||
|
|
||||||
export type TableName =
|
export type TableName =
|
||||||
| "guilds"
|
| "guilds"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { eventHandlers } from "../../bot.ts";
|
import { eventHandlers } from "../../bot.ts";
|
||||||
import { structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
ChannelCreatePayload,
|
ChannelCreatePayload,
|
||||||
ChannelTypes,
|
ChannelTypes,
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
|
import { structures } from "../structures/structures.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalChannelCreate(data: DiscordPayload) {
|
export async function handleInternalChannelCreate(data: DiscordPayload) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { eventHandlers } from "../../bot.ts";
|
import { eventHandlers } from "../../bot.ts";
|
||||||
import { structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
CreateGuildPayload,
|
CreateGuildPayload,
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
@@ -9,6 +8,7 @@ import {
|
|||||||
UpdateGuildPayload,
|
UpdateGuildPayload,
|
||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
import { cache } from "../../util/cache.ts";
|
import { cache } from "../../util/cache.ts";
|
||||||
|
import { structures } from "../structures/structures.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalGuildCreate(
|
export async function handleInternalGuildCreate(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { DiscordPayload } from "../types/types.ts";
|
import { eventHandlers } from "../../bot.ts";
|
||||||
import { eventHandlers } from "../module/client.ts";
|
import {
|
||||||
|
DiscordPayload,
|
||||||
|
InteractionCommandPayload,
|
||||||
|
} from "../../types/types.ts";
|
||||||
import { structures } from "../structures/mod.ts";
|
import { structures } from "../structures/mod.ts";
|
||||||
import { InteractionCommandPayload } from "../types/types.ts";
|
|
||||||
|
|
||||||
export async function handleInternalInteractionsCreate(data: DiscordPayload) {
|
export async function handleInternalInteractionsCreate(data: DiscordPayload) {
|
||||||
if (data.t !== "INTERACTION_CREATE") return;
|
if (data.t !== "INTERACTION_CREATE") return;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { eventHandlers } from "../../bot.ts";
|
import { eventHandlers } from "../../bot.ts";
|
||||||
import { structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
GuildBanPayload,
|
GuildBanPayload,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
GuildMemberUpdatePayload,
|
GuildMemberUpdatePayload,
|
||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
import { cache } from "../../util/cache.ts";
|
import { cache } from "../../util/cache.ts";
|
||||||
|
import { structures } from "../structures/structures.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalGuildMemberAdd(data: DiscordPayload) {
|
export async function handleInternalGuildMemberAdd(data: DiscordPayload) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { eventHandlers } from "../../bot.ts";
|
import { eventHandlers } from "../../bot.ts";
|
||||||
import { structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
MessageCreateOptions,
|
MessageCreateOptions,
|
||||||
MessageDeleteBulkPayload,
|
MessageDeleteBulkPayload,
|
||||||
MessageDeletePayload,
|
MessageDeletePayload,
|
||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
|
import { structures } from "../structures/structures.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalMessageCreate(data: DiscordPayload) {
|
export async function handleInternalMessageCreate(data: DiscordPayload) {
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
|
||||||
initialMemberLoadQueue,
|
|
||||||
structures,
|
|
||||||
} from "../structures/structures.ts";
|
|
||||||
import { eventHandlers, setBotID } from "../../bot.ts";
|
import { eventHandlers, setBotID } from "../../bot.ts";
|
||||||
import { allowNextShard } from "../../ws/shard_manager.ts";
|
|
||||||
import {
|
import {
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
PresenceUpdatePayload,
|
PresenceUpdatePayload,
|
||||||
@@ -15,6 +10,11 @@ import {
|
|||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
import { cache } from "../../util/cache.ts";
|
import { cache } from "../../util/cache.ts";
|
||||||
import { delay } from "../../util/utils.ts";
|
import { delay } from "../../util/utils.ts";
|
||||||
|
import { allowNextShard } from "../../ws/shard_manager.ts";
|
||||||
|
import {
|
||||||
|
initialMemberLoadQueue,
|
||||||
|
structures,
|
||||||
|
} from "../structures/structures.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalReady(
|
export async function handleInternalReady(
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import {
|
|||||||
handleInternalGuildEmojisUpdate,
|
handleInternalGuildEmojisUpdate,
|
||||||
handleInternalGuildUpdate,
|
handleInternalGuildUpdate,
|
||||||
} from "./guilds.ts";
|
} from "./guilds.ts";
|
||||||
|
import {
|
||||||
|
handleInternalInteractionsCommandCreate,
|
||||||
|
handleInternalInteractionsCreate,
|
||||||
|
} from "./interactions.ts";
|
||||||
import {
|
import {
|
||||||
handleInternalGuildMemberAdd,
|
handleInternalGuildMemberAdd,
|
||||||
handleInternalGuildMemberRemove,
|
handleInternalGuildMemberRemove,
|
||||||
@@ -44,10 +48,6 @@ import {
|
|||||||
handleInternalGuildRoleDelete,
|
handleInternalGuildRoleDelete,
|
||||||
handleInternalGuildRoleUpdate,
|
handleInternalGuildRoleUpdate,
|
||||||
} from "./roles.ts";
|
} from "./roles.ts";
|
||||||
import {
|
|
||||||
handleInternalInteractionsCommandCreate,
|
|
||||||
handleInternalInteractionsCreate,
|
|
||||||
} from "./interactions.ts";
|
|
||||||
|
|
||||||
export let controllers = {
|
export let controllers = {
|
||||||
READY: handleInternalReady,
|
READY: handleInternalReady,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { botID, eventHandlers } from "../../bot.ts";
|
import { botID, eventHandlers } from "../../bot.ts";
|
||||||
import { structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
BaseMessageReactionPayload,
|
BaseMessageReactionPayload,
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
MessageReactionPayload,
|
MessageReactionPayload,
|
||||||
MessageReactionRemoveEmojiPayload,
|
MessageReactionRemoveEmojiPayload,
|
||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
|
import { structures } from "../structures/structures.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalMessageReactionAdd(data: DiscordPayload) {
|
export async function handleInternalMessageReactionAdd(data: DiscordPayload) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { eventHandlers } from "../../bot.ts";
|
import { eventHandlers } from "../../bot.ts";
|
||||||
import { structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
GuildRoleDeletePayload,
|
GuildRoleDeletePayload,
|
||||||
GuildRolePayload,
|
GuildRolePayload,
|
||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
|
import { structures } from "../structures/structures.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalGuildRoleCreate(data: DiscordPayload) {
|
export async function handleInternalGuildRoleCreate(data: DiscordPayload) {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { cacheHandlers } from "../controllers/cache.ts";
|
|
||||||
import { RequestManager } from "../../rest/mod.ts";
|
import { RequestManager } from "../../rest/mod.ts";
|
||||||
import { structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
ChannelEditOptions,
|
ChannelEditOptions,
|
||||||
ChannelTypes,
|
ChannelTypes,
|
||||||
@@ -23,6 +21,8 @@ import {
|
|||||||
botHasChannelPermissions,
|
botHasChannelPermissions,
|
||||||
calculateBits,
|
calculateBits,
|
||||||
} from "../../util/permissions.ts";
|
} from "../../util/permissions.ts";
|
||||||
|
import { cacheHandlers } from "../controllers/cache.ts";
|
||||||
|
import { structures } from "../structures/structures.ts";
|
||||||
|
|
||||||
/** Checks if a channel overwrite for a user id or a role id has permission in this channel */
|
/** Checks if a channel overwrite for a user id or a role id has permission in this channel */
|
||||||
export function channelOverwriteHasPermission(
|
export function channelOverwriteHasPermission(
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { cacheHandlers } from "../controllers/cache.ts";
|
|
||||||
import { identifyPayload } from "../../bot.ts";
|
import { identifyPayload } from "../../bot.ts";
|
||||||
import { RequestManager } from "../../rest/mod.ts";
|
import { RequestManager } from "../../rest/mod.ts";
|
||||||
import { requestAllMembers } from "../../ws/shard_manager.ts";
|
|
||||||
import {
|
|
||||||
Guild,
|
|
||||||
Member,
|
|
||||||
structures,
|
|
||||||
Template,
|
|
||||||
} from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
AuditLogs,
|
AuditLogs,
|
||||||
BannedUser,
|
BannedUser,
|
||||||
@@ -40,11 +32,18 @@ import {
|
|||||||
UpdateGuildPayload,
|
UpdateGuildPayload,
|
||||||
UserPayload,
|
UserPayload,
|
||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
import { formatImageURL } from "../../util/cdn.ts";
|
|
||||||
import { Collection } from "../../util/collection.ts";
|
import { Collection } from "../../util/collection.ts";
|
||||||
import { endpoints } from "../../util/constants.ts";
|
import { endpoints } from "../../util/constants.ts";
|
||||||
import { botHasPermission, calculateBits } from "../../util/permissions.ts";
|
import { botHasPermission, calculateBits } from "../../util/permissions.ts";
|
||||||
import { urlToBase64 } from "../../util/utils.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,
|
||||||
|
Template,
|
||||||
|
} from "../structures/structures.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. */
|
/** 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) {
|
export async function createServer(options: CreateServerOptions) {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { cacheHandlers } from "../controllers/cache.ts";
|
|
||||||
import { botID } from "../../bot.ts";
|
import { botID } from "../../bot.ts";
|
||||||
import { RequestManager } from "../../rest/mod.ts";
|
import { RequestManager } from "../../rest/mod.ts";
|
||||||
import { Member, structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
DMChannelCreatePayload,
|
DMChannelCreatePayload,
|
||||||
EditMemberOptions,
|
EditMemberOptions,
|
||||||
@@ -10,14 +8,15 @@ import {
|
|||||||
ImageSize,
|
ImageSize,
|
||||||
MessageContent,
|
MessageContent,
|
||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
import { formatImageURL } from "../../util/cdn.ts";
|
|
||||||
import { endpoints } from "../../util/constants.ts";
|
import { endpoints } from "../../util/constants.ts";
|
||||||
import {
|
import {
|
||||||
botHasPermission,
|
botHasPermission,
|
||||||
higherRolePosition,
|
higherRolePosition,
|
||||||
highestRole,
|
highestRole,
|
||||||
} from "../../util/permissions.ts";
|
} from "../../util/permissions.ts";
|
||||||
import { urlToBase64 } from "../../util/utils.ts";
|
import { formatImageURL, urlToBase64 } from "../../util/utils.ts";
|
||||||
|
import { cacheHandlers } from "../controllers/cache.ts";
|
||||||
|
import { Member, structures } from "../structures/structures.ts";
|
||||||
import { sendMessage } from "./channel.ts";
|
import { sendMessage } from "./channel.ts";
|
||||||
|
|
||||||
/** The users custom avatar or the default avatar if you don't have a member object. */
|
/** The users custom avatar or the default avatar if you don't have a member object. */
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { cacheHandlers } from "../controllers/cache.ts";
|
|
||||||
import { botID } from "../../bot.ts";
|
import { botID } from "../../bot.ts";
|
||||||
import { RequestManager } from "../../rest/mod.ts";
|
import { RequestManager } from "../../rest/mod.ts";
|
||||||
import { Message, structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
Errors,
|
Errors,
|
||||||
MessageContent,
|
MessageContent,
|
||||||
@@ -11,6 +9,8 @@ import {
|
|||||||
import { endpoints } from "../../util/constants.ts";
|
import { endpoints } from "../../util/constants.ts";
|
||||||
import { botHasChannelPermissions } from "../../util/permissions.ts";
|
import { botHasChannelPermissions } from "../../util/permissions.ts";
|
||||||
import { delay } from "../../util/utils.ts";
|
import { delay } from "../../util/utils.ts";
|
||||||
|
import { cacheHandlers } from "../controllers/cache.ts";
|
||||||
|
import { Message, structures } from "../structures/structures.ts";
|
||||||
|
|
||||||
/** Delete a message with the channel id and message id only. */
|
/** Delete a message with the channel id and message id only. */
|
||||||
export async function deleteMessageByID(
|
export async function deleteMessageByID(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { botID } from "../../bot.ts";
|
||||||
import { RequestManager } from "../../rest/mod.ts";
|
import { RequestManager } from "../../rest/mod.ts";
|
||||||
import { structures } from "../structures/structures.ts";
|
|
||||||
import {
|
import {
|
||||||
CreateSlashCommandOptions,
|
CreateSlashCommandOptions,
|
||||||
EditSlashCommandOptions,
|
EditSlashCommandOptions,
|
||||||
@@ -13,11 +13,11 @@ import {
|
|||||||
WebhookCreateOptions,
|
WebhookCreateOptions,
|
||||||
WebhookPayload,
|
WebhookPayload,
|
||||||
} from "../../types/types.ts";
|
} from "../../types/types.ts";
|
||||||
|
import { cache } from "../../util/cache.ts";
|
||||||
import { endpoints } from "../../util/constants.ts";
|
import { endpoints } from "../../util/constants.ts";
|
||||||
import { botHasChannelPermissions } from "../../util/permissions.ts";
|
import { botHasChannelPermissions } from "../../util/permissions.ts";
|
||||||
import { urlToBase64 } from "../../util/utils.ts";
|
import { urlToBase64 } from "../../util/utils.ts";
|
||||||
import { botID } from "../../bot.ts";
|
import { structures } from "../structures/structures.ts";
|
||||||
import { cache } from "../../util/cache.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:
|
/** 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:
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -28,3 +28,10 @@ export function updateStructures(newStructures: Structures) {
|
|||||||
...newStructures,
|
...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";
|
||||||
|
|||||||
+9
-9
@@ -1,10 +1,10 @@
|
|||||||
|
import { RequestManager } from "./rest/mod.ts";
|
||||||
import {
|
import {
|
||||||
ClientOptions,
|
BotConfig,
|
||||||
DiscordBotGatewayData,
|
DiscordBotGatewayData,
|
||||||
EventHandlers,
|
EventHandlers,
|
||||||
} from "./types/types.ts";
|
} from "./types/types.ts";
|
||||||
import { baseEndpoints, endpoints } from "./util/constants.ts";
|
import { baseEndpoints, endpoints } from "./util/constants.ts";
|
||||||
import { RequestManager } from "./rest/mod.ts";
|
|
||||||
import { spawnShards } from "./ws/shard_manager.ts";
|
import { spawnShards } from "./ws/shard_manager.ts";
|
||||||
|
|
||||||
export let authorization = "";
|
export let authorization = "";
|
||||||
@@ -39,9 +39,9 @@ export interface IdentifyPayload {
|
|||||||
shard: [number, number];
|
shard: [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startBot(data: ClientOptions) {
|
export async function startBot(config: BotConfig) {
|
||||||
if (data.eventHandlers) eventHandlers = data.eventHandlers;
|
if (config.eventHandlers) eventHandlers = config.eventHandlers;
|
||||||
authorization = `Bot ${data.token}`;
|
authorization = `Bot ${config.token}`;
|
||||||
|
|
||||||
// Initial API connection to get info about bots connection
|
// Initial API connection to get info about bots connection
|
||||||
botGatewayData = await RequestManager.get(
|
botGatewayData = await RequestManager.get(
|
||||||
@@ -49,8 +49,8 @@ export async function startBot(data: ClientOptions) {
|
|||||||
) as DiscordBotGatewayData;
|
) as DiscordBotGatewayData;
|
||||||
|
|
||||||
proxyWSURL = botGatewayData.url;
|
proxyWSURL = botGatewayData.url;
|
||||||
identifyPayload.token = data.token;
|
identifyPayload.token = config.token;
|
||||||
identifyPayload.intents = data.intents.reduce(
|
identifyPayload.intents = config.intents.reduce(
|
||||||
(bits, next) => (bits |= next),
|
(bits, next) => (bits |= next),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
@@ -75,7 +75,7 @@ export function setBotID(id: string) {
|
|||||||
*
|
*
|
||||||
* Advanced Devs: This function will allow you to have an insane amount of customization potential as when you get to large bots you need to be able to optimize every tiny detail to make you bot work the way you need.
|
* Advanced Devs: This function will allow you to have an insane amount of customization potential as when you get to large bots you need to be able to optimize every tiny detail to make you bot work the way you need.
|
||||||
*/
|
*/
|
||||||
export async function startBigBrainBot(data: BigBrainBotOptions) {
|
export async function startBigBrainBot(data: BigBrainBotConfig) {
|
||||||
authorization = `Bot ${data.token}`;
|
authorization = `Bot ${data.token}`;
|
||||||
identifyPayload.token = `Bot ${data.token}`;
|
identifyPayload.token = `Bot ${data.token}`;
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ export async function startBigBrainBot(data: BigBrainBotOptions) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BigBrainBotOptions extends ClientOptions {
|
export interface BigBrainBotConfig extends BotConfig {
|
||||||
/** The first shard to start at for this worker. Use this to control which shards to run in each worker. */
|
/** The first shard to start at for this worker. Use this to control which shards to run in each worker. */
|
||||||
firstShardID: number;
|
firstShardID: number;
|
||||||
/** The last shard to start for this worker. By default it will be 25 + the firstShardID. */
|
/** The last shard to start for this worker. By default it will be 25 + the firstShardID. */
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./interactions.ts";
|
export * from "./server.ts";
|
||||||
export * from "./types/mod.ts";
|
export * from "./types/mod.ts";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Embed } from "./embed.ts";
|
import { Embed } from "./embed.ts";
|
||||||
import { AllowedMentions } from "./misc.ts";
|
|
||||||
import { MemberCreatePayload } from "./member.ts";
|
import { MemberCreatePayload } from "./member.ts";
|
||||||
|
import { AllowedMentions } from "./misc.ts";
|
||||||
|
|
||||||
export interface Interaction {
|
export interface Interaction {
|
||||||
/** The id of the interaction */
|
/** The id of the interaction */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from "./embed.ts";
|
export * from "./embed.ts";
|
||||||
export * from "./interactions.ts";
|
export * from "./interactions.ts";
|
||||||
|
export * from "./member.ts";
|
||||||
export * from "./misc.ts";
|
export * from "./misc.ts";
|
||||||
export * from "./slash.ts";
|
export * from "./slash.ts";
|
||||||
export * from "./member.ts";
|
|
||||||
export * from "./webhook.ts";
|
export * from "./webhook.ts";
|
||||||
|
|||||||
+1
-418
@@ -1,418 +1 @@
|
|||||||
import { Errors, HttpResponseCode, RequestMethods } from "../types/types.ts";
|
export * from "./request_manager.ts";
|
||||||
import { baseEndpoints, discordAPIURLS } from "../util/constants.ts";
|
|
||||||
import { delay } from "../util/utils.ts";
|
|
||||||
import { authorization, eventHandlers } from "../bot.ts";
|
|
||||||
|
|
||||||
const pathQueues: { [key: string]: QueuedRequest[] } = {};
|
|
||||||
const ratelimitedPaths = new Map<string, RateLimitedPath>();
|
|
||||||
let globallyRateLimited = false;
|
|
||||||
let queueInProcess = false;
|
|
||||||
|
|
||||||
export interface QueuedRequest {
|
|
||||||
callback: () => Promise<
|
|
||||||
void | {
|
|
||||||
rateLimited: any;
|
|
||||||
beforeFetch: boolean;
|
|
||||||
bucketID?: string | null;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
bucketID?: string | null;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RateLimitedPath {
|
|
||||||
url: string;
|
|
||||||
resetTimestamp: number;
|
|
||||||
bucketID: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processRateLimitedPaths() {
|
|
||||||
const now = Date.now();
|
|
||||||
ratelimitedPaths.forEach((value, key) => {
|
|
||||||
if (value.resetTimestamp > now) return;
|
|
||||||
ratelimitedPaths.delete(key);
|
|
||||||
if (key === "global") globallyRateLimited = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
await delay(1000);
|
|
||||||
processRateLimitedPaths();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addToQueue(request: QueuedRequest) {
|
|
||||||
const route = request.url.substring(baseEndpoints.BASE_URL.length + 1);
|
|
||||||
const parts = route.split("/");
|
|
||||||
// Remove the major param
|
|
||||||
parts.shift();
|
|
||||||
const [id] = parts;
|
|
||||||
|
|
||||||
if (pathQueues[id]) {
|
|
||||||
pathQueues[id].push(request);
|
|
||||||
} else {
|
|
||||||
pathQueues[id] = [request];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanupQueues() {
|
|
||||||
Object.entries(pathQueues).map(([key, value]) => {
|
|
||||||
if (!value.length) {
|
|
||||||
// Remove it entirely
|
|
||||||
delete pathQueues[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processQueue() {
|
|
||||||
while (queueInProcess) {
|
|
||||||
if (
|
|
||||||
(Object.keys(pathQueues).length) && !globallyRateLimited
|
|
||||||
) {
|
|
||||||
await Promise.allSettled(
|
|
||||||
Object.values(pathQueues).map(async (pathQueue) => {
|
|
||||||
const request = pathQueue.shift();
|
|
||||||
if (!request) return;
|
|
||||||
|
|
||||||
const rateLimitedURLResetIn = await checkRatelimits(request.url);
|
|
||||||
|
|
||||||
if (request.bucketID) {
|
|
||||||
const rateLimitResetIn = await checkRatelimits(request.bucketID);
|
|
||||||
if (rateLimitResetIn) {
|
|
||||||
// This request is still rate limited readd to queue
|
|
||||||
addToQueue(request);
|
|
||||||
} else if (rateLimitedURLResetIn) {
|
|
||||||
// This URL is rate limited readd to queue
|
|
||||||
addToQueue(request);
|
|
||||||
} else {
|
|
||||||
// This request is not rate limited so it should be run
|
|
||||||
const result = await request.callback();
|
|
||||||
if (result && result.rateLimited) {
|
|
||||||
addToQueue(
|
|
||||||
{ ...request, bucketID: result.bucketID || request.bucketID },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rateLimitedURLResetIn) {
|
|
||||||
// This URL is rate limited readd to queue
|
|
||||||
addToQueue(request);
|
|
||||||
} else {
|
|
||||||
// This request has no bucket id so it should be processed
|
|
||||||
const result = await request.callback();
|
|
||||||
if (request && result && result.rateLimited) {
|
|
||||||
addToQueue(
|
|
||||||
{ ...request, bucketID: result.bucketID || request.bucketID },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(pathQueues).length) {
|
|
||||||
cleanupQueues();
|
|
||||||
} else queueInProcess = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processRateLimitedPaths();
|
|
||||||
|
|
||||||
export const RequestManager = {
|
|
||||||
get: async (url: string, body?: unknown) => {
|
|
||||||
return runMethod("get", url, body);
|
|
||||||
},
|
|
||||||
post: (url: string, body?: unknown) => {
|
|
||||||
return runMethod("post", url, body);
|
|
||||||
},
|
|
||||||
delete: (url: string, body?: unknown) => {
|
|
||||||
return runMethod("delete", url, body);
|
|
||||||
},
|
|
||||||
patch: (url: string, body?: unknown) => {
|
|
||||||
return runMethod("patch", url, body);
|
|
||||||
},
|
|
||||||
put: (url: string, body?: unknown) => {
|
|
||||||
return runMethod("put", url, body);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createRequestBody(body: any, method: RequestMethods) {
|
|
||||||
const headers: { [key: string]: string } = {
|
|
||||||
Authorization: authorization,
|
|
||||||
"User-Agent":
|
|
||||||
`DiscordBot (https://github.com/skillz4killz/discordeno, v10)`,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (method === "get") body = undefined;
|
|
||||||
|
|
||||||
if (body?.reason) {
|
|
||||||
headers["X-Audit-Log-Reason"] = encodeURIComponent(body.reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body?.file) {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append("file", body.file.blob, body.file.name);
|
|
||||||
form.append("payload_json", JSON.stringify({ ...body, file: undefined }));
|
|
||||||
body.file = form;
|
|
||||||
} else if (
|
|
||||||
body && !["get", "delete"].includes(method)
|
|
||||||
) {
|
|
||||||
headers["Content-Type"] = "application/json";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
headers,
|
|
||||||
body: body?.file || JSON.stringify(body),
|
|
||||||
method: method.toUpperCase(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkRatelimits(url: string) {
|
|
||||||
const ratelimited = ratelimitedPaths.get(url);
|
|
||||||
const global = ratelimitedPaths.get("global");
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (ratelimited && now < ratelimited.resetTimestamp) {
|
|
||||||
return ratelimited.resetTimestamp - now;
|
|
||||||
}
|
|
||||||
if (global && now < global.resetTimestamp) {
|
|
||||||
return global.resetTimestamp - now;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runMethod(
|
|
||||||
method: RequestMethods,
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
retryCount = 0,
|
|
||||||
bucketID?: string | null,
|
|
||||||
) {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "requestManager",
|
|
||||||
data: { method, url, body, retryCount, bucketID },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const errorStack = new Error("Location:");
|
|
||||||
Error.captureStackTrace(errorStack);
|
|
||||||
|
|
||||||
// For proxies we don't need to do any of the legwork so we just forward the request
|
|
||||||
if (
|
|
||||||
!url.startsWith(discordAPIURLS.BASE_URL) &&
|
|
||||||
!url.startsWith(discordAPIURLS.CDN_URL)
|
|
||||||
) {
|
|
||||||
return fetch(url, { method, body: body ? JSON.stringify(body) : undefined })
|
|
||||||
.then((res) => res.json())
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
throw errorStack;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// No proxy so we need to handl all rate limiting and such
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const callback = async () => {
|
|
||||||
try {
|
|
||||||
const rateLimitResetIn = await checkRatelimits(url);
|
|
||||||
if (rateLimitResetIn) {
|
|
||||||
return { rateLimited: rateLimitResetIn, beforeFetch: true, bucketID };
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = method === "get" && body
|
|
||||||
? Object.entries(body as any).map(([key, value]) =>
|
|
||||||
`${encodeURIComponent(key)}=${encodeURIComponent(value as any)}`
|
|
||||||
)
|
|
||||||
.join("&")
|
|
||||||
: "";
|
|
||||||
const urlToUse = method === "get" && query ? `${url}?${query}` : url;
|
|
||||||
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "requestManagerFetching",
|
|
||||||
data: { method, url, body, retryCount, bucketID },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const response = await fetch(urlToUse, createRequestBody(body, method));
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "requestManagerFetched",
|
|
||||||
data: { method, url, body, retryCount, bucketID, response },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const bucketIDFromHeaders = processHeaders(url, response.headers);
|
|
||||||
handleStatusCode(response, errorStack);
|
|
||||||
|
|
||||||
// Sometimes Discord returns an empty 204 response that can't be made to JSON.
|
|
||||||
if (response.status === 204) return resolve(undefined);
|
|
||||||
|
|
||||||
const json = await response.json();
|
|
||||||
if (
|
|
||||||
json.retry_after ||
|
|
||||||
json.message === "You are being rate limited."
|
|
||||||
) {
|
|
||||||
if (retryCount > 10) {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
data: { method, url, body, retryCount, bucketID, errorStack },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
throw new Error(Errors.RATE_LIMIT_RETRY_MAXED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
rateLimited: json.retry_after,
|
|
||||||
beforeFetch: false,
|
|
||||||
bucketID: bucketIDFromHeaders,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "requestManagerSuccess",
|
|
||||||
data: { method, url, body, retryCount, bucketID },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return resolve(json);
|
|
||||||
} catch (error) {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
data: { method, url, body, retryCount, bucketID, errorStack },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return reject(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
addToQueue({
|
|
||||||
callback,
|
|
||||||
bucketID,
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
if (!queueInProcess) {
|
|
||||||
queueInProcess = true;
|
|
||||||
processQueue();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logErrors(response: Response, errorStack?: unknown) {
|
|
||||||
try {
|
|
||||||
const error = await response.json();
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
eventHandlers.debug?.({ type: "error", data: { errorStack, error } });
|
|
||||||
} catch {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
data: { errorStack },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
console.error(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStatusCode(response: Response, errorStack?: unknown) {
|
|
||||||
const status = response.status;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(status >= 200 && status < 400) ||
|
|
||||||
status === HttpResponseCode.TooManyRequests
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
logErrors(response, errorStack);
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case HttpResponseCode.BadRequest:
|
|
||||||
console.error(
|
|
||||||
"The request was improperly formatted, or the server couldn't understand it.",
|
|
||||||
);
|
|
||||||
throw errorStack;
|
|
||||||
case HttpResponseCode.Unauthorized:
|
|
||||||
console.error("The Authorization header was missing or invalid.");
|
|
||||||
throw errorStack;
|
|
||||||
case HttpResponseCode.Forbidden:
|
|
||||||
console.error(
|
|
||||||
"The Authorization token you passed did not have permission to the resource.",
|
|
||||||
);
|
|
||||||
throw errorStack;
|
|
||||||
case HttpResponseCode.NotFound:
|
|
||||||
console.error("The resource at the location specified doesn't exist.");
|
|
||||||
throw errorStack;
|
|
||||||
case HttpResponseCode.MethodNotAllowed:
|
|
||||||
console.error(
|
|
||||||
"The HTTP method used is not valid for the location specified.",
|
|
||||||
);
|
|
||||||
throw errorStack;
|
|
||||||
case HttpResponseCode.GatewayUnavailable:
|
|
||||||
console.error(
|
|
||||||
"There was not a gateway available to process your request. Wait a bit and retry.",
|
|
||||||
);
|
|
||||||
throw errorStack;
|
|
||||||
// left are all unknown
|
|
||||||
default:
|
|
||||||
console.error(Errors.REQUEST_UNKNOWN_ERROR);
|
|
||||||
throw errorStack;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processHeaders(url: string, headers: Headers) {
|
|
||||||
let ratelimited = false;
|
|
||||||
|
|
||||||
// Get all useful headers
|
|
||||||
const remaining = headers.get("x-ratelimit-remaining");
|
|
||||||
const resetTimestamp = headers.get("x-ratelimit-reset");
|
|
||||||
const retryAfter = headers.get("retry-after");
|
|
||||||
const global = headers.get("x-ratelimit-global");
|
|
||||||
const bucketID = headers.get("x-ratelimit-bucket");
|
|
||||||
|
|
||||||
// If there is no remaining rate limit for this endpoint, we save it in cache
|
|
||||||
if (remaining && remaining === "0") {
|
|
||||||
ratelimited = true;
|
|
||||||
|
|
||||||
ratelimitedPaths.set(url, {
|
|
||||||
url,
|
|
||||||
resetTimestamp: Number(resetTimestamp) * 1000,
|
|
||||||
bucketID,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bucketID) {
|
|
||||||
ratelimitedPaths.set(bucketID, {
|
|
||||||
url,
|
|
||||||
resetTimestamp: Number(resetTimestamp) * 1000,
|
|
||||||
bucketID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no remaining global limit, we save it in cache
|
|
||||||
if (global) {
|
|
||||||
const reset = Date.now() + (Number(retryAfter) * 1000);
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{ type: "globallyRateLimited", data: { url, reset } },
|
|
||||||
);
|
|
||||||
globallyRateLimited = true;
|
|
||||||
ratelimited = true;
|
|
||||||
|
|
||||||
ratelimitedPaths.set("global", {
|
|
||||||
url: "global",
|
|
||||||
resetTimestamp: reset,
|
|
||||||
bucketID,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bucketID) {
|
|
||||||
ratelimitedPaths.set(bucketID, {
|
|
||||||
url: "global",
|
|
||||||
resetTimestamp: reset,
|
|
||||||
bucketID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ratelimited ? bucketID : undefined;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,417 @@
|
|||||||
|
import { authorization, eventHandlers } from "../bot.ts";
|
||||||
|
import { Errors, HttpResponseCode, RequestMethods } from "../types/types.ts";
|
||||||
|
import { API_VERSION, baseEndpoints, BASE_URL, IMAGE_BASE_URL, USER_AGENT } from "../util/constants.ts";
|
||||||
|
import { delay } from "../util/utils.ts";
|
||||||
|
|
||||||
|
const pathQueues: { [key: string]: QueuedRequest[] } = {};
|
||||||
|
const ratelimitedPaths = new Map<string, RateLimitedPath>();
|
||||||
|
let globallyRateLimited = false;
|
||||||
|
let queueInProcess = false;
|
||||||
|
|
||||||
|
export interface QueuedRequest {
|
||||||
|
callback: () => Promise<
|
||||||
|
void | {
|
||||||
|
rateLimited: any;
|
||||||
|
beforeFetch: boolean;
|
||||||
|
bucketID?: string | null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
bucketID?: string | null;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateLimitedPath {
|
||||||
|
url: string;
|
||||||
|
resetTimestamp: number;
|
||||||
|
bucketID: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processRateLimitedPaths() {
|
||||||
|
const now = Date.now();
|
||||||
|
ratelimitedPaths.forEach((value, key) => {
|
||||||
|
if (value.resetTimestamp > now) return;
|
||||||
|
ratelimitedPaths.delete(key);
|
||||||
|
if (key === "global") globallyRateLimited = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
await delay(1000);
|
||||||
|
processRateLimitedPaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToQueue(request: QueuedRequest) {
|
||||||
|
const route = request.url.substring(baseEndpoints.BASE_URL.length + 1);
|
||||||
|
const parts = route.split("/");
|
||||||
|
// Remove the major param
|
||||||
|
parts.shift();
|
||||||
|
const [id] = parts;
|
||||||
|
|
||||||
|
if (pathQueues[id]) {
|
||||||
|
pathQueues[id].push(request);
|
||||||
|
} else {
|
||||||
|
pathQueues[id] = [request];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupQueues() {
|
||||||
|
Object.entries(pathQueues).map(([key, value]) => {
|
||||||
|
if (!value.length) {
|
||||||
|
// Remove it entirely
|
||||||
|
delete pathQueues[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processQueue() {
|
||||||
|
while (queueInProcess) {
|
||||||
|
if (
|
||||||
|
(Object.keys(pathQueues).length) && !globallyRateLimited
|
||||||
|
) {
|
||||||
|
await Promise.allSettled(
|
||||||
|
Object.values(pathQueues).map(async (pathQueue) => {
|
||||||
|
const request = pathQueue.shift();
|
||||||
|
if (!request) return;
|
||||||
|
|
||||||
|
const rateLimitedURLResetIn = await checkRatelimits(request.url);
|
||||||
|
|
||||||
|
if (request.bucketID) {
|
||||||
|
const rateLimitResetIn = await checkRatelimits(request.bucketID);
|
||||||
|
if (rateLimitResetIn) {
|
||||||
|
// This request is still rate limited readd to queue
|
||||||
|
addToQueue(request);
|
||||||
|
} else if (rateLimitedURLResetIn) {
|
||||||
|
// This URL is rate limited readd to queue
|
||||||
|
addToQueue(request);
|
||||||
|
} else {
|
||||||
|
// This request is not rate limited so it should be run
|
||||||
|
const result = await request.callback();
|
||||||
|
if (result && result.rateLimited) {
|
||||||
|
addToQueue(
|
||||||
|
{ ...request, bucketID: result.bucketID || request.bucketID },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (rateLimitedURLResetIn) {
|
||||||
|
// This URL is rate limited readd to queue
|
||||||
|
addToQueue(request);
|
||||||
|
} else {
|
||||||
|
// This request has no bucket id so it should be processed
|
||||||
|
const result = await request.callback();
|
||||||
|
if (request && result && result.rateLimited) {
|
||||||
|
addToQueue(
|
||||||
|
{ ...request, bucketID: result.bucketID || request.bucketID },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(pathQueues).length) {
|
||||||
|
cleanupQueues();
|
||||||
|
} else queueInProcess = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processRateLimitedPaths();
|
||||||
|
|
||||||
|
export const RequestManager = {
|
||||||
|
get: async (url: string, body?: unknown) => {
|
||||||
|
return runMethod("get", url, body);
|
||||||
|
},
|
||||||
|
post: (url: string, body?: unknown) => {
|
||||||
|
return runMethod("post", url, body);
|
||||||
|
},
|
||||||
|
delete: (url: string, body?: unknown) => {
|
||||||
|
return runMethod("delete", url, body);
|
||||||
|
},
|
||||||
|
patch: (url: string, body?: unknown) => {
|
||||||
|
return runMethod("patch", url, body);
|
||||||
|
},
|
||||||
|
put: (url: string, body?: unknown) => {
|
||||||
|
return runMethod("put", url, body);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createRequestBody(body: any, method: RequestMethods) {
|
||||||
|
const headers: { [key: string]: string } = {
|
||||||
|
Authorization: authorization,
|
||||||
|
"User-Agent": USER_AGENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method === "get") body = undefined;
|
||||||
|
|
||||||
|
if (body?.reason) {
|
||||||
|
headers["X-Audit-Log-Reason"] = encodeURIComponent(body.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body?.file) {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file", body.file.blob, body.file.name);
|
||||||
|
form.append("payload_json", JSON.stringify({ ...body, file: undefined }));
|
||||||
|
body.file = form;
|
||||||
|
} else if (
|
||||||
|
body && !["get", "delete"].includes(method)
|
||||||
|
) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers,
|
||||||
|
body: body?.file || JSON.stringify(body),
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkRatelimits(url: string) {
|
||||||
|
const ratelimited = ratelimitedPaths.get(url);
|
||||||
|
const global = ratelimitedPaths.get("global");
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (ratelimited && now < ratelimited.resetTimestamp) {
|
||||||
|
return ratelimited.resetTimestamp - now;
|
||||||
|
}
|
||||||
|
if (global && now < global.resetTimestamp) {
|
||||||
|
return global.resetTimestamp - now;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMethod(
|
||||||
|
method: RequestMethods,
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
retryCount = 0,
|
||||||
|
bucketID?: string | null,
|
||||||
|
) {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "requestManager",
|
||||||
|
data: { method, url, body, retryCount, bucketID },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorStack = new Error("Location:");
|
||||||
|
Error.captureStackTrace(errorStack);
|
||||||
|
|
||||||
|
// For proxies we don't need to do any of the legwork so we just forward the request
|
||||||
|
if (
|
||||||
|
!url.startsWith(`${BASE_URL}/v${API_VERSION}`) &&
|
||||||
|
!url.startsWith(IMAGE_BASE_URL)
|
||||||
|
) {
|
||||||
|
return fetch(url, { method, body: body ? JSON.stringify(body) : undefined })
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
throw errorStack;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// No proxy so we need to handl all rate limiting and such
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const callback = async () => {
|
||||||
|
try {
|
||||||
|
const rateLimitResetIn = await checkRatelimits(url);
|
||||||
|
if (rateLimitResetIn) {
|
||||||
|
return { rateLimited: rateLimitResetIn, beforeFetch: true, bucketID };
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = method === "get" && body
|
||||||
|
? Object.entries(body as any).map(([key, value]) =>
|
||||||
|
`${encodeURIComponent(key)}=${encodeURIComponent(value as any)}`
|
||||||
|
)
|
||||||
|
.join("&")
|
||||||
|
: "";
|
||||||
|
const urlToUse = method === "get" && query ? `${url}?${query}` : url;
|
||||||
|
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "requestManagerFetching",
|
||||||
|
data: { method, url, body, retryCount, bucketID },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const response = await fetch(urlToUse, createRequestBody(body, method));
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "requestManagerFetched",
|
||||||
|
data: { method, url, body, retryCount, bucketID, response },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const bucketIDFromHeaders = processHeaders(url, response.headers);
|
||||||
|
handleStatusCode(response, errorStack);
|
||||||
|
|
||||||
|
// Sometimes Discord returns an empty 204 response that can't be made to JSON.
|
||||||
|
if (response.status === 204) return resolve(undefined);
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
if (
|
||||||
|
json.retry_after ||
|
||||||
|
json.message === "You are being rate limited."
|
||||||
|
) {
|
||||||
|
if (retryCount > 10) {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "error",
|
||||||
|
data: { method, url, body, retryCount, bucketID, errorStack },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
throw new Error(Errors.RATE_LIMIT_RETRY_MAXED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rateLimited: json.retry_after,
|
||||||
|
beforeFetch: false,
|
||||||
|
bucketID: bucketIDFromHeaders,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "requestManagerSuccess",
|
||||||
|
data: { method, url, body, retryCount, bucketID },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return resolve(json);
|
||||||
|
} catch (error) {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "error",
|
||||||
|
data: { method, url, body, retryCount, bucketID, errorStack },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addToQueue({
|
||||||
|
callback,
|
||||||
|
bucketID,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
if (!queueInProcess) {
|
||||||
|
queueInProcess = true;
|
||||||
|
processQueue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logErrors(response: Response, errorStack?: unknown) {
|
||||||
|
try {
|
||||||
|
const error = await response.json();
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
eventHandlers.debug?.({ type: "error", data: { errorStack, error } });
|
||||||
|
} catch {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "error",
|
||||||
|
data: { errorStack },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.error(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStatusCode(response: Response, errorStack?: unknown) {
|
||||||
|
const status = response.status;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(status >= 200 && status < 400) ||
|
||||||
|
status === HttpResponseCode.TooManyRequests
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logErrors(response, errorStack);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case HttpResponseCode.BadRequest:
|
||||||
|
console.error(
|
||||||
|
"The request was improperly formatted, or the server couldn't understand it.",
|
||||||
|
);
|
||||||
|
throw errorStack;
|
||||||
|
case HttpResponseCode.Unauthorized:
|
||||||
|
console.error("The Authorization header was missing or invalid.");
|
||||||
|
throw errorStack;
|
||||||
|
case HttpResponseCode.Forbidden:
|
||||||
|
console.error(
|
||||||
|
"The Authorization token you passed did not have permission to the resource.",
|
||||||
|
);
|
||||||
|
throw errorStack;
|
||||||
|
case HttpResponseCode.NotFound:
|
||||||
|
console.error("The resource at the location specified doesn't exist.");
|
||||||
|
throw errorStack;
|
||||||
|
case HttpResponseCode.MethodNotAllowed:
|
||||||
|
console.error(
|
||||||
|
"The HTTP method used is not valid for the location specified.",
|
||||||
|
);
|
||||||
|
throw errorStack;
|
||||||
|
case HttpResponseCode.GatewayUnavailable:
|
||||||
|
console.error(
|
||||||
|
"There was not a gateway available to process your request. Wait a bit and retry.",
|
||||||
|
);
|
||||||
|
throw errorStack;
|
||||||
|
// left are all unknown
|
||||||
|
default:
|
||||||
|
console.error(Errors.REQUEST_UNKNOWN_ERROR);
|
||||||
|
throw errorStack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processHeaders(url: string, headers: Headers) {
|
||||||
|
let ratelimited = false;
|
||||||
|
|
||||||
|
// Get all useful headers
|
||||||
|
const remaining = headers.get("x-ratelimit-remaining");
|
||||||
|
const resetTimestamp = headers.get("x-ratelimit-reset");
|
||||||
|
const retryAfter = headers.get("retry-after");
|
||||||
|
const global = headers.get("x-ratelimit-global");
|
||||||
|
const bucketID = headers.get("x-ratelimit-bucket");
|
||||||
|
|
||||||
|
// If there is no remaining rate limit for this endpoint, we save it in cache
|
||||||
|
if (remaining && remaining === "0") {
|
||||||
|
ratelimited = true;
|
||||||
|
|
||||||
|
ratelimitedPaths.set(url, {
|
||||||
|
url,
|
||||||
|
resetTimestamp: Number(resetTimestamp) * 1000,
|
||||||
|
bucketID,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bucketID) {
|
||||||
|
ratelimitedPaths.set(bucketID, {
|
||||||
|
url,
|
||||||
|
resetTimestamp: Number(resetTimestamp) * 1000,
|
||||||
|
bucketID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no remaining global limit, we save it in cache
|
||||||
|
if (global) {
|
||||||
|
const reset = Date.now() + (Number(retryAfter) * 1000);
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{ type: "globallyRateLimited", data: { url, reset } },
|
||||||
|
);
|
||||||
|
globallyRateLimited = true;
|
||||||
|
ratelimited = true;
|
||||||
|
|
||||||
|
ratelimitedPaths.set("global", {
|
||||||
|
url: "global",
|
||||||
|
resetTimestamp: reset,
|
||||||
|
bucketID,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bucketID) {
|
||||||
|
ratelimitedPaths.set(bucketID, {
|
||||||
|
url: "global",
|
||||||
|
resetTimestamp: reset,
|
||||||
|
bucketID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ratelimited ? bucketID : undefined;
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { Guild } from "../api/structures/structures.ts";
|
import { Guild } from "../api/structures/mod.ts";
|
||||||
import { ChannelCreatePayload, ChannelTypes } from "./channel.ts";
|
import { ChannelCreatePayload, ChannelTypes } from "./channel.ts";
|
||||||
import { Emoji, StatusType } from "./discord.ts";
|
import { Emoji, StatusType } from "./discord.ts";
|
||||||
import { MemberCreatePayload } from "./member.ts";
|
import { MemberCreatePayload } from "./member.ts";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Channel } from "../api/structures/structures.ts";
|
import { Channel } from "../api/structures/mod.ts";
|
||||||
import { ChannelType } from "./channel.ts";
|
import { ChannelType } from "./channel.ts";
|
||||||
import { UserPayload } from "./guild.ts";
|
import { UserPayload } from "./guild.ts";
|
||||||
import { MemberCreatePayload } from "./member.ts";
|
import { MemberCreatePayload } from "./member.ts";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
Member,
|
Member,
|
||||||
Message,
|
Message,
|
||||||
Role,
|
Role,
|
||||||
} from "../api/structures/structures.ts";
|
} from "../api/structures/mod.ts";
|
||||||
import {
|
import {
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
Emoji,
|
Emoji,
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
ReactionPayload,
|
ReactionPayload,
|
||||||
} from "./message.ts";
|
} from "./message.ts";
|
||||||
|
|
||||||
export interface ClientOptions {
|
export interface BotConfig {
|
||||||
token: string;
|
token: string;
|
||||||
compress?: boolean;
|
compress?: boolean;
|
||||||
intents: Intents[];
|
intents: Intents[];
|
||||||
|
|||||||
+1
-1
@@ -5,6 +5,7 @@ export * from "./discord.ts";
|
|||||||
export * from "./errors.ts";
|
export * from "./errors.ts";
|
||||||
export * from "./fetch.ts";
|
export * from "./fetch.ts";
|
||||||
export * from "./guild.ts";
|
export * from "./guild.ts";
|
||||||
|
export * from "./interactions.ts";
|
||||||
export * from "./member.ts";
|
export * from "./member.ts";
|
||||||
export * from "./message.ts";
|
export * from "./message.ts";
|
||||||
export * from "./misc.ts";
|
export * from "./misc.ts";
|
||||||
@@ -13,4 +14,3 @@ export * from "./permission.ts";
|
|||||||
export * from "./presence.ts";
|
export * from "./presence.ts";
|
||||||
export * from "./role.ts";
|
export * from "./role.ts";
|
||||||
export * from "./webhook.ts";
|
export * from "./webhook.ts";
|
||||||
export * from "./interactions.ts";
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { ImageFormats, ImageSize } from "../types/types.ts";
|
|
||||||
|
|
||||||
export const formatImageURL = (
|
|
||||||
url: string,
|
|
||||||
size: ImageSize = 128,
|
|
||||||
format?: ImageFormats,
|
|
||||||
) => {
|
|
||||||
return `${url}.${format ||
|
|
||||||
(url.includes("/a_") ? "gif" : "jpg")}?size=${size}`;
|
|
||||||
};
|
|
||||||
+13
-7
@@ -1,13 +1,19 @@
|
|||||||
// These will never be modified and remain constants
|
/** https://discord.com/developers/docs/reference#api-reference-base-url */
|
||||||
export const discordAPIURLS = {
|
export const BASE_URL = "https://discord.com/api";
|
||||||
BASE_URL: `https://discord.com/api/v8`,
|
|
||||||
CDN_URL: "https://cdn.discordapp.com",
|
/** https://discord.com/developers/docs/reference#api-versioning-api-versions */
|
||||||
};
|
export const API_VERSION = 8;
|
||||||
|
|
||||||
|
/** https://discord.com/developers/docs/reference#user-agent */
|
||||||
|
export const USER_AGENT = "DiscordBot (https://github.com/discordeno/discordeno, v10)";
|
||||||
|
|
||||||
|
/** https://discord.com/developers/docs/reference#image-formatting-image-base-url */
|
||||||
|
export const IMAGE_BASE_URL = "https://cdn.discordapp.com/";
|
||||||
|
|
||||||
// This can be modified by big brain bots and use a proxy
|
// This can be modified by big brain bots and use a proxy
|
||||||
export const baseEndpoints = {
|
export const baseEndpoints = {
|
||||||
BASE_URL: discordAPIURLS.BASE_URL,
|
BASE_URL: `${BASE_URL}/v${API_VERSION}`,
|
||||||
CDN_URL: discordAPIURLS.CDN_URL,
|
CDN_URL: IMAGE_BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
const GUILDS_BASE = (id: string) => `${baseEndpoints.BASE_URL}/guilds/${id}`;
|
const GUILDS_BASE = (id: string) => `${baseEndpoints.BASE_URL}/guilds/${id}`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { cacheHandlers } from "../api/controllers/cache.ts";
|
import { cacheHandlers } from "../api/controllers/cache.ts";
|
||||||
import { botID } from "../bot.ts";
|
|
||||||
import { Guild, Role } from "../api/structures/structures.ts";
|
import { Guild, Role } from "../api/structures/structures.ts";
|
||||||
|
import { botID } from "../bot.ts";
|
||||||
import { Permission, Permissions, RawOverwrite } from "../types/types.ts";
|
import { Permission, Permissions, RawOverwrite } from "../types/types.ts";
|
||||||
|
|
||||||
/** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */
|
/** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */
|
||||||
|
|||||||
+15
-1
@@ -1,5 +1,10 @@
|
|||||||
import { encode } from "../../deps.ts";
|
import { encode } from "../../deps.ts";
|
||||||
import { ActivityType, StatusType } from "../types/types.ts";
|
import {
|
||||||
|
ActivityType,
|
||||||
|
ImageFormats,
|
||||||
|
ImageSize,
|
||||||
|
StatusType,
|
||||||
|
} from "../types/types.ts";
|
||||||
import { sendGatewayCommand } from "../ws/shard_manager.ts";
|
import { sendGatewayCommand } from "../ws/shard_manager.ts";
|
||||||
|
|
||||||
export const sleep = (timeout: number) => {
|
export const sleep = (timeout: number) => {
|
||||||
@@ -45,3 +50,12 @@ export function delay(ms: number): Promise<void> {
|
|||||||
}, ms)
|
}, ms)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatImageURL = (
|
||||||
|
url: string,
|
||||||
|
size: ImageSize = 128,
|
||||||
|
format?: ImageFormats,
|
||||||
|
) => {
|
||||||
|
return `${url}.${format ||
|
||||||
|
(url.includes("/a_") ? "gif" : "jpg")}?size=${size}`;
|
||||||
|
};
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export { decompress_with as decompressWith } from "https://unpkg.com/@evan/wasm@0.0.22/target/zlib/deno.js";
|
export { decompress_with as decompressWith } from "https://unpkg.com/@evan/wasm@0.0.25/target/zlib/deno.js";
|
||||||
|
|||||||
+2
-436
@@ -1,436 +1,2 @@
|
|||||||
import { botGatewayData, eventHandlers } from "../bot.ts";
|
export * from "./shard.ts";
|
||||||
import {
|
export * from "./shard_manager.ts";
|
||||||
DiscordBotGatewayData,
|
|
||||||
DiscordHeartbeatPayload,
|
|
||||||
FetchMembersOptions,
|
|
||||||
GatewayOpcode,
|
|
||||||
ReadyPayload,
|
|
||||||
} from "../types/types.ts";
|
|
||||||
import { BotStatusRequest, delay } from "../util/utils.ts";
|
|
||||||
import { IdentifyPayload, proxyWSURL } from "../bot.ts";
|
|
||||||
import { handleDiscordPayload } from "./shard_manager.ts";
|
|
||||||
import { decompressWith } from "./deps.ts";
|
|
||||||
|
|
||||||
const basicShards = new Map<number, BasicShard>();
|
|
||||||
const heartbeating = new Map<number, boolean>();
|
|
||||||
const utf8decoder = new TextDecoder();
|
|
||||||
const RequestMembersQueue: RequestMemberQueuedRequest[] = [];
|
|
||||||
let processQueue = false;
|
|
||||||
|
|
||||||
export interface BasicShard {
|
|
||||||
id: number;
|
|
||||||
socket: WebSocket;
|
|
||||||
resumeInterval: number;
|
|
||||||
sessionID: string;
|
|
||||||
previousSequenceNumber: number | null;
|
|
||||||
needToResume: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RequestMemberQueuedRequest {
|
|
||||||
guildID: string;
|
|
||||||
shardID: number;
|
|
||||||
nonce: string;
|
|
||||||
options?: FetchMembersOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createShard(
|
|
||||||
data: DiscordBotGatewayData,
|
|
||||||
identifyPayload: IdentifyPayload,
|
|
||||||
resuming = false,
|
|
||||||
shardID = 0,
|
|
||||||
) {
|
|
||||||
const oldShard = basicShards.get(shardID);
|
|
||||||
|
|
||||||
const socket = new WebSocket(proxyWSURL);
|
|
||||||
socket.binaryType = "arraybuffer";
|
|
||||||
const basicShard: BasicShard = {
|
|
||||||
id: shardID,
|
|
||||||
socket,
|
|
||||||
resumeInterval: 0,
|
|
||||||
sessionID: oldShard?.sessionID || "",
|
|
||||||
previousSequenceNumber: oldShard?.previousSequenceNumber || 0,
|
|
||||||
needToResume: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
basicShards.set(basicShard.id, basicShard);
|
|
||||||
|
|
||||||
socket.onopen = async () => {
|
|
||||||
if (!resuming) {
|
|
||||||
// Initial identify with the gateway
|
|
||||||
await identify(basicShard, identifyPayload);
|
|
||||||
} else {
|
|
||||||
await resume(basicShard, identifyPayload);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onerror = ({ timeStamp }) => {
|
|
||||||
eventHandlers.debug?.({ type: "wsError", data: { timeStamp } });
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onmessage = ({ data: message }) => {
|
|
||||||
if (message instanceof ArrayBuffer) {
|
|
||||||
message = new Uint8Array(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message instanceof Uint8Array) {
|
|
||||||
message = decompressWith(
|
|
||||||
message,
|
|
||||||
0,
|
|
||||||
(slice: Uint8Array) => utf8decoder.decode(slice),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof message === "string") {
|
|
||||||
const data = JSON.parse(message);
|
|
||||||
if (!data.t) eventHandlers.rawGateway?.(data);
|
|
||||||
switch (data.op) {
|
|
||||||
case GatewayOpcode.Hello:
|
|
||||||
if (!heartbeating.has(basicShard.id)) {
|
|
||||||
heartbeat(
|
|
||||||
basicShard,
|
|
||||||
(data.d as DiscordHeartbeatPayload).heartbeat_interval,
|
|
||||||
identifyPayload,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case GatewayOpcode.HeartbeatACK:
|
|
||||||
heartbeating.set(shardID, true);
|
|
||||||
break;
|
|
||||||
case GatewayOpcode.Reconnect:
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{ type: "reconnect", data: { shardID: basicShard.id } },
|
|
||||||
);
|
|
||||||
basicShard.needToResume = true;
|
|
||||||
resumeConnection(data, identifyPayload, basicShard.id);
|
|
||||||
break;
|
|
||||||
case GatewayOpcode.InvalidSession:
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{ type: "invalidSession", data: { shardID: basicShard.id, data } },
|
|
||||||
);
|
|
||||||
// When d is false we need to reidentify
|
|
||||||
if (!data.d) {
|
|
||||||
createShard(data, identifyPayload, false, shardID);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
basicShard.needToResume = true;
|
|
||||||
resumeConnection(data, identifyPayload, basicShard.id);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (data.t === "RESUMED") {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{ type: "resumed", data: { shardID: basicShard.id } },
|
|
||||||
);
|
|
||||||
|
|
||||||
basicShard.needToResume = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Important for RESUME
|
|
||||||
if (data.t === "READY") {
|
|
||||||
basicShard.sessionID = (data.d as ReadyPayload).session_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the sequence number if it is present
|
|
||||||
if (data.s) basicShard.previousSequenceNumber = data.s;
|
|
||||||
|
|
||||||
handleDiscordPayload(data, basicShard.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO(ayntee): better ws* event names
|
|
||||||
socket.onclose = ({ reason, code, wasClean }) => {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "wsClose",
|
|
||||||
data: { shardID: basicShard.id, code, reason, wasClean },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (code) {
|
|
||||||
case 4001:
|
|
||||||
throw new Error(
|
|
||||||
"[Unknown opcode] Sent an invalid Gateway opcode or an invalid payload for an opcode.",
|
|
||||||
);
|
|
||||||
case 4002:
|
|
||||||
throw new Error("[Decode error] Sent an invalid payload to API.");
|
|
||||||
case 4004:
|
|
||||||
throw new Error(
|
|
||||||
"[Authentication failed] The account token sent with your identify payload is incorrect.",
|
|
||||||
);
|
|
||||||
case 4005:
|
|
||||||
throw new Error(
|
|
||||||
"[Already authenticated] Sent more than one identify payload.",
|
|
||||||
);
|
|
||||||
case 4010:
|
|
||||||
throw new Error(
|
|
||||||
"[Invalid shard] Sent an invalid shard when identifying.",
|
|
||||||
);
|
|
||||||
case 4011:
|
|
||||||
throw new Error(
|
|
||||||
"[Sharding required] The session would have handled too many guilds - you are required to shard your connection in order to connect.",
|
|
||||||
);
|
|
||||||
case 4012:
|
|
||||||
throw new Error(
|
|
||||||
"[Invalid API version] Sent an invalid version for the gateway.",
|
|
||||||
);
|
|
||||||
case 4013:
|
|
||||||
throw new Error(
|
|
||||||
"[Invalid intent(s)] Sent an invalid intent for a Gateway Intent.",
|
|
||||||
);
|
|
||||||
case 4014:
|
|
||||||
throw new Error(
|
|
||||||
"[Disallowed intent(s)] Sent a disallowed intent for a Gateway Intent. You may have tried to specify an intent that you have not enabled or are not whitelisted for.",
|
|
||||||
);
|
|
||||||
case 4003:
|
|
||||||
case 4007:
|
|
||||||
case 4008:
|
|
||||||
case 4009:
|
|
||||||
eventHandlers.debug?.({
|
|
||||||
type: "wsReconnect",
|
|
||||||
data: { shardID: basicShard.id, code, reason, wasClean },
|
|
||||||
});
|
|
||||||
createShard(data, identifyPayload, false, shardID);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
basicShard.needToResume = true;
|
|
||||||
resumeConnection(botGatewayData, identifyPayload, shardID);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function identify(shard: BasicShard, payload: IdentifyPayload) {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "identifying",
|
|
||||||
data: {
|
|
||||||
shardID: shard.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return shard.socket.send(
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
op: GatewayOpcode.Identify,
|
|
||||||
d: { ...payload, shard: [shard.id, payload.shard[1]] },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resume(shard: BasicShard, payload: IdentifyPayload) {
|
|
||||||
return shard.socket.send(JSON.stringify({
|
|
||||||
op: GatewayOpcode.Resume,
|
|
||||||
d: {
|
|
||||||
token: payload.token,
|
|
||||||
session_id: shard.sessionID,
|
|
||||||
seq: shard.previousSequenceNumber,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function heartbeat(
|
|
||||||
shard: BasicShard,
|
|
||||||
interval: number,
|
|
||||||
payload: IdentifyPayload,
|
|
||||||
data: DiscordBotGatewayData,
|
|
||||||
) {
|
|
||||||
// We lost socket connection between heartbeats, resume connection
|
|
||||||
if (shard.socket.readyState === WebSocket.CLOSED) {
|
|
||||||
shard.needToResume = true;
|
|
||||||
resumeConnection(data, payload, shard.id);
|
|
||||||
heartbeating.delete(shard.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heartbeating.has(shard.id)) {
|
|
||||||
const receivedACK = heartbeating.get(shard.id);
|
|
||||||
// If a ACK response was not received since last heartbeat, issue invalid session close
|
|
||||||
if (!receivedACK) {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "heartbeatStopped",
|
|
||||||
data: {
|
|
||||||
interval,
|
|
||||||
previousSequenceNumber: shard.previousSequenceNumber,
|
|
||||||
shardID: shard.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return shard.socket.send(JSON.stringify({ op: 4009 }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set it to false as we are issuing a new heartbeat
|
|
||||||
heartbeating.set(shard.id, false);
|
|
||||||
|
|
||||||
shard.socket.send(
|
|
||||||
JSON.stringify(
|
|
||||||
{ op: GatewayOpcode.Heartbeat, d: shard.previousSequenceNumber },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "heartbeat",
|
|
||||||
data: {
|
|
||||||
interval,
|
|
||||||
previousSequenceNumber: shard.previousSequenceNumber,
|
|
||||||
shardID: shard.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await delay(interval);
|
|
||||||
heartbeat(shard, interval, payload, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resumeConnection(
|
|
||||||
data: DiscordBotGatewayData,
|
|
||||||
payload: IdentifyPayload,
|
|
||||||
shardID: number,
|
|
||||||
) {
|
|
||||||
const shard = basicShards.get(shardID);
|
|
||||||
if (!shard) {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{ type: "missingShard", data: { shardID: shardID } },
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shard.needToResume) return;
|
|
||||||
|
|
||||||
eventHandlers.debug?.({ type: "resuming", data: { shardID: shard.id } });
|
|
||||||
// Run it once
|
|
||||||
createShard(data, payload, true, shard.id);
|
|
||||||
// Then retry every 15 seconds
|
|
||||||
await delay(1000 * 15);
|
|
||||||
if (shard.needToResume) resumeConnection(data, payload, shardID);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function requestGuildMembers(
|
|
||||||
guildID: string,
|
|
||||||
shardID: number,
|
|
||||||
nonce: string,
|
|
||||||
options?: FetchMembersOptions,
|
|
||||||
queuedRequest = false,
|
|
||||||
) {
|
|
||||||
const shard = basicShards.get(shardID);
|
|
||||||
|
|
||||||
// This request was not from this queue so we add it to queue first
|
|
||||||
if (!queuedRequest) {
|
|
||||||
RequestMembersQueue.push({
|
|
||||||
guildID,
|
|
||||||
shardID,
|
|
||||||
nonce,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!processQueue) {
|
|
||||||
processQueue = true;
|
|
||||||
processGatewayQueue();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If its closed add back to queue to redo on resume
|
|
||||||
if (shard?.socket.readyState === WebSocket.CLOSED) {
|
|
||||||
requestGuildMembers(guildID, shardID, nonce, options);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
shard?.socket.send(JSON.stringify({
|
|
||||||
op: GatewayOpcode.RequestGuildMembers,
|
|
||||||
d: {
|
|
||||||
guild_id: guildID,
|
|
||||||
// If a query is provided use it, OR if a limit is NOT provided use ""
|
|
||||||
query: options?.query || (options?.limit ? undefined : ""),
|
|
||||||
limit: options?.limit || 0,
|
|
||||||
presences: options?.presences || false,
|
|
||||||
user_ids: options?.userIDs,
|
|
||||||
nonce,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processGatewayQueue() {
|
|
||||||
if (!RequestMembersQueue.length) {
|
|
||||||
processQueue = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
basicShards.forEach((shard) => {
|
|
||||||
const index = RequestMembersQueue.findIndex((q) => q.shardID === shard.id);
|
|
||||||
// 2 events per second is the rate limit.
|
|
||||||
const request = RequestMembersQueue[index];
|
|
||||||
if (request) {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "requestMembersProcessing",
|
|
||||||
data: {
|
|
||||||
remaining: RequestMembersQueue.length,
|
|
||||||
request,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
requestGuildMembers(
|
|
||||||
request.guildID,
|
|
||||||
request.shardID,
|
|
||||||
request.nonce,
|
|
||||||
request.options,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
// Remove item from queue
|
|
||||||
RequestMembersQueue.splice(index, 1);
|
|
||||||
|
|
||||||
const secondIndex = RequestMembersQueue.findIndex((q) =>
|
|
||||||
q.shardID === shard.id
|
|
||||||
);
|
|
||||||
const secondRequest = RequestMembersQueue[secondIndex];
|
|
||||||
if (secondRequest) {
|
|
||||||
eventHandlers.debug?.(
|
|
||||||
{
|
|
||||||
type: "requestMembersProcessing",
|
|
||||||
data: {
|
|
||||||
remaining: RequestMembersQueue.length,
|
|
||||||
request,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
requestGuildMembers(
|
|
||||||
secondRequest.guildID,
|
|
||||||
secondRequest.shardID,
|
|
||||||
secondRequest.nonce,
|
|
||||||
secondRequest.options,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
// Remove item from queue
|
|
||||||
RequestMembersQueue.splice(secondIndex, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await delay(1500);
|
|
||||||
|
|
||||||
processGatewayQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function botGatewayStatusRequest(payload: BotStatusRequest) {
|
|
||||||
basicShards.forEach((shard) => {
|
|
||||||
shard.socket.send(JSON.stringify({
|
|
||||||
op: GatewayOpcode.StatusUpdate,
|
|
||||||
d: {
|
|
||||||
since: null,
|
|
||||||
game: payload.game.name
|
|
||||||
? {
|
|
||||||
name: payload.game.name,
|
|
||||||
type: payload.game.type,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
status: payload.status,
|
|
||||||
afk: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
+439
@@ -0,0 +1,439 @@
|
|||||||
|
import {
|
||||||
|
botGatewayData,
|
||||||
|
eventHandlers,
|
||||||
|
IdentifyPayload,
|
||||||
|
proxyWSURL,
|
||||||
|
} from "../bot.ts";
|
||||||
|
import {
|
||||||
|
DiscordBotGatewayData,
|
||||||
|
DiscordHeartbeatPayload,
|
||||||
|
FetchMembersOptions,
|
||||||
|
GatewayOpcode,
|
||||||
|
ReadyPayload,
|
||||||
|
} from "../types/types.ts";
|
||||||
|
import { BotStatusRequest, delay } from "../util/utils.ts";
|
||||||
|
import { decompressWith } from "./deps.ts";
|
||||||
|
import { handleDiscordPayload } from "./shard_manager.ts";
|
||||||
|
|
||||||
|
const basicShards = new Map<number, BasicShard>();
|
||||||
|
const heartbeating = new Map<number, boolean>();
|
||||||
|
const utf8decoder = new TextDecoder();
|
||||||
|
const RequestMembersQueue: RequestMemberQueuedRequest[] = [];
|
||||||
|
let processQueue = false;
|
||||||
|
|
||||||
|
export interface BasicShard {
|
||||||
|
id: number;
|
||||||
|
ws: WebSocket;
|
||||||
|
resumeInterval: number;
|
||||||
|
sessionID: string;
|
||||||
|
previousSequenceNumber: number | null;
|
||||||
|
needToResume: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestMemberQueuedRequest {
|
||||||
|
guildID: string;
|
||||||
|
shardID: number;
|
||||||
|
nonce: string;
|
||||||
|
options?: FetchMembersOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createShard(
|
||||||
|
data: DiscordBotGatewayData,
|
||||||
|
identifyPayload: IdentifyPayload,
|
||||||
|
resuming = false,
|
||||||
|
shardID = 0,
|
||||||
|
) {
|
||||||
|
const oldShard = basicShards.get(shardID);
|
||||||
|
|
||||||
|
const ws = new WebSocket(proxyWSURL);
|
||||||
|
ws.binaryType = "arraybuffer";
|
||||||
|
const basicShard: BasicShard = {
|
||||||
|
id: shardID,
|
||||||
|
ws,
|
||||||
|
resumeInterval: 0,
|
||||||
|
sessionID: oldShard?.sessionID || "",
|
||||||
|
previousSequenceNumber: oldShard?.previousSequenceNumber || 0,
|
||||||
|
needToResume: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
basicShards.set(basicShard.id, basicShard);
|
||||||
|
|
||||||
|
ws.onopen = async () => {
|
||||||
|
if (!resuming) {
|
||||||
|
// Initial identify with the gateway
|
||||||
|
await identify(basicShard, identifyPayload);
|
||||||
|
} else {
|
||||||
|
await resume(basicShard, identifyPayload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = ({ timeStamp }) => {
|
||||||
|
eventHandlers.debug?.({ type: "wsError", data: { timeStamp } });
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = ({ data: message }) => {
|
||||||
|
if (message instanceof ArrayBuffer) {
|
||||||
|
message = new Uint8Array(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message instanceof Uint8Array) {
|
||||||
|
message = decompressWith(
|
||||||
|
message,
|
||||||
|
0,
|
||||||
|
(slice: Uint8Array) => utf8decoder.decode(slice),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof message === "string") {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
if (!data.t) eventHandlers.rawGateway?.(data);
|
||||||
|
switch (data.op) {
|
||||||
|
case GatewayOpcode.Hello:
|
||||||
|
if (!heartbeating.has(basicShard.id)) {
|
||||||
|
heartbeat(
|
||||||
|
basicShard,
|
||||||
|
(data.d as DiscordHeartbeatPayload).heartbeat_interval,
|
||||||
|
identifyPayload,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GatewayOpcode.HeartbeatACK:
|
||||||
|
heartbeating.set(shardID, true);
|
||||||
|
break;
|
||||||
|
case GatewayOpcode.Reconnect:
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{ type: "reconnect", data: { shardID: basicShard.id } },
|
||||||
|
);
|
||||||
|
basicShard.needToResume = true;
|
||||||
|
resumeConnection(data, identifyPayload, basicShard.id);
|
||||||
|
break;
|
||||||
|
case GatewayOpcode.InvalidSession:
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{ type: "invalidSession", data: { shardID: basicShard.id, data } },
|
||||||
|
);
|
||||||
|
// When d is false we need to reidentify
|
||||||
|
if (!data.d) {
|
||||||
|
createShard(data, identifyPayload, false, shardID);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
basicShard.needToResume = true;
|
||||||
|
resumeConnection(data, identifyPayload, basicShard.id);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (data.t === "RESUMED") {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{ type: "resumed", data: { shardID: basicShard.id } },
|
||||||
|
);
|
||||||
|
|
||||||
|
basicShard.needToResume = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Important for RESUME
|
||||||
|
if (data.t === "READY") {
|
||||||
|
basicShard.sessionID = (data.d as ReadyPayload).session_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the sequence number if it is present
|
||||||
|
if (data.s) basicShard.previousSequenceNumber = data.s;
|
||||||
|
|
||||||
|
handleDiscordPayload(data, basicShard.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = ({ reason, code, wasClean }) => {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "wsClose",
|
||||||
|
data: { shardID: basicShard.id, code, reason, wasClean },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (code) {
|
||||||
|
case 4001:
|
||||||
|
throw new Error(
|
||||||
|
"[Unknown opcode] Sent an invalid Gateway opcode or an invalid payload for an opcode.",
|
||||||
|
);
|
||||||
|
case 4002:
|
||||||
|
throw new Error("[Decode error] Sent an invalid payload to API.");
|
||||||
|
case 4004:
|
||||||
|
throw new Error(
|
||||||
|
"[Authentication failed] The account token sent with your identify payload is incorrect.",
|
||||||
|
);
|
||||||
|
case 4005:
|
||||||
|
throw new Error(
|
||||||
|
"[Already authenticated] Sent more than one identify payload.",
|
||||||
|
);
|
||||||
|
case 4010:
|
||||||
|
throw new Error(
|
||||||
|
"[Invalid shard] Sent an invalid shard when identifying.",
|
||||||
|
);
|
||||||
|
case 4011:
|
||||||
|
throw new Error(
|
||||||
|
"[Sharding required] The session would have handled too many guilds - you are required to shard your connection in order to connect.",
|
||||||
|
);
|
||||||
|
case 4012:
|
||||||
|
throw new Error(
|
||||||
|
"[Invalid API version] Sent an invalid version for the gateway.",
|
||||||
|
);
|
||||||
|
case 4013:
|
||||||
|
throw new Error(
|
||||||
|
"[Invalid intent(s)] Sent an invalid intent for a Gateway Intent.",
|
||||||
|
);
|
||||||
|
case 4014:
|
||||||
|
throw new Error(
|
||||||
|
"[Disallowed intent(s)] Sent a disallowed intent for a Gateway Intent. You may have tried to specify an intent that you have not enabled or are not whitelisted for.",
|
||||||
|
);
|
||||||
|
case 4003:
|
||||||
|
case 4007:
|
||||||
|
case 4008:
|
||||||
|
case 4009:
|
||||||
|
eventHandlers.debug?.({
|
||||||
|
type: "wsReconnect",
|
||||||
|
data: { shardID: basicShard.id, code, reason, wasClean },
|
||||||
|
});
|
||||||
|
createShard(data, identifyPayload, false, shardID);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
basicShard.needToResume = true;
|
||||||
|
resumeConnection(botGatewayData, identifyPayload, shardID);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function identify(shard: BasicShard, payload: IdentifyPayload) {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "identifying",
|
||||||
|
data: {
|
||||||
|
shardID: shard.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return shard.ws.send(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
op: GatewayOpcode.Identify,
|
||||||
|
d: { ...payload, shard: [shard.id, payload.shard[1]] },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resume(shard: BasicShard, payload: IdentifyPayload) {
|
||||||
|
return shard.ws.send(JSON.stringify({
|
||||||
|
op: GatewayOpcode.Resume,
|
||||||
|
d: {
|
||||||
|
token: payload.token,
|
||||||
|
session_id: shard.sessionID,
|
||||||
|
seq: shard.previousSequenceNumber,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function heartbeat(
|
||||||
|
shard: BasicShard,
|
||||||
|
interval: number,
|
||||||
|
payload: IdentifyPayload,
|
||||||
|
data: DiscordBotGatewayData,
|
||||||
|
) {
|
||||||
|
// We lost socket connection between heartbeats, resume connection
|
||||||
|
if (shard.ws.readyState === WebSocket.CLOSED) {
|
||||||
|
shard.needToResume = true;
|
||||||
|
resumeConnection(data, payload, shard.id);
|
||||||
|
heartbeating.delete(shard.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeating.has(shard.id)) {
|
||||||
|
const receivedACK = heartbeating.get(shard.id);
|
||||||
|
// If a ACK response was not received since last heartbeat, issue invalid session close
|
||||||
|
if (!receivedACK) {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "heartbeatStopped",
|
||||||
|
data: {
|
||||||
|
interval,
|
||||||
|
previousSequenceNumber: shard.previousSequenceNumber,
|
||||||
|
shardID: shard.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return shard.ws.send(JSON.stringify({ op: 4009 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set it to false as we are issuing a new heartbeat
|
||||||
|
heartbeating.set(shard.id, false);
|
||||||
|
|
||||||
|
shard.ws.send(
|
||||||
|
JSON.stringify(
|
||||||
|
{ op: GatewayOpcode.Heartbeat, d: shard.previousSequenceNumber },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "heartbeat",
|
||||||
|
data: {
|
||||||
|
interval,
|
||||||
|
previousSequenceNumber: shard.previousSequenceNumber,
|
||||||
|
shardID: shard.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await delay(interval);
|
||||||
|
heartbeat(shard, interval, payload, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resumeConnection(
|
||||||
|
data: DiscordBotGatewayData,
|
||||||
|
payload: IdentifyPayload,
|
||||||
|
shardID: number,
|
||||||
|
) {
|
||||||
|
const shard = basicShards.get(shardID);
|
||||||
|
if (!shard) {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{ type: "missingShard", data: { shardID: shardID } },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shard.needToResume) return;
|
||||||
|
|
||||||
|
eventHandlers.debug?.({ type: "resuming", data: { shardID: shard.id } });
|
||||||
|
// Run it once
|
||||||
|
createShard(data, payload, true, shard.id);
|
||||||
|
// Then retry every 15 seconds
|
||||||
|
await delay(1000 * 15);
|
||||||
|
if (shard.needToResume) resumeConnection(data, payload, shardID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestGuildMembers(
|
||||||
|
guildID: string,
|
||||||
|
shardID: number,
|
||||||
|
nonce: string,
|
||||||
|
options?: FetchMembersOptions,
|
||||||
|
queuedRequest = false,
|
||||||
|
) {
|
||||||
|
const shard = basicShards.get(shardID);
|
||||||
|
|
||||||
|
// This request was not from this queue so we add it to queue first
|
||||||
|
if (!queuedRequest) {
|
||||||
|
RequestMembersQueue.push({
|
||||||
|
guildID,
|
||||||
|
shardID,
|
||||||
|
nonce,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!processQueue) {
|
||||||
|
processQueue = true;
|
||||||
|
processGatewayQueue();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If its closed add back to queue to redo on resume
|
||||||
|
if (shard?.ws.readyState === WebSocket.CLOSED) {
|
||||||
|
requestGuildMembers(guildID, shardID, nonce, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shard?.ws.send(JSON.stringify({
|
||||||
|
op: GatewayOpcode.RequestGuildMembers,
|
||||||
|
d: {
|
||||||
|
guild_id: guildID,
|
||||||
|
// If a query is provided use it, OR if a limit is NOT provided use ""
|
||||||
|
query: options?.query || (options?.limit ? undefined : ""),
|
||||||
|
limit: options?.limit || 0,
|
||||||
|
presences: options?.presences || false,
|
||||||
|
user_ids: options?.userIDs,
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processGatewayQueue() {
|
||||||
|
if (!RequestMembersQueue.length) {
|
||||||
|
processQueue = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
basicShards.forEach((shard) => {
|
||||||
|
const index = RequestMembersQueue.findIndex((q) => q.shardID === shard.id);
|
||||||
|
// 2 events per second is the rate limit.
|
||||||
|
const request = RequestMembersQueue[index];
|
||||||
|
if (request) {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "requestMembersProcessing",
|
||||||
|
data: {
|
||||||
|
remaining: RequestMembersQueue.length,
|
||||||
|
request,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
requestGuildMembers(
|
||||||
|
request.guildID,
|
||||||
|
request.shardID,
|
||||||
|
request.nonce,
|
||||||
|
request.options,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
// Remove item from queue
|
||||||
|
RequestMembersQueue.splice(index, 1);
|
||||||
|
|
||||||
|
const secondIndex = RequestMembersQueue.findIndex((q) =>
|
||||||
|
q.shardID === shard.id
|
||||||
|
);
|
||||||
|
const secondRequest = RequestMembersQueue[secondIndex];
|
||||||
|
if (secondRequest) {
|
||||||
|
eventHandlers.debug?.(
|
||||||
|
{
|
||||||
|
type: "requestMembersProcessing",
|
||||||
|
data: {
|
||||||
|
remaining: RequestMembersQueue.length,
|
||||||
|
request,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
requestGuildMembers(
|
||||||
|
secondRequest.guildID,
|
||||||
|
secondRequest.shardID,
|
||||||
|
secondRequest.nonce,
|
||||||
|
secondRequest.options,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
// Remove item from queue
|
||||||
|
RequestMembersQueue.splice(secondIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await delay(1500);
|
||||||
|
|
||||||
|
processGatewayQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function botGatewayStatusRequest(payload: BotStatusRequest) {
|
||||||
|
basicShards.forEach((shard) => {
|
||||||
|
shard.ws.send(JSON.stringify({
|
||||||
|
op: GatewayOpcode.StatusUpdate,
|
||||||
|
d: {
|
||||||
|
since: null,
|
||||||
|
game: payload.game.name
|
||||||
|
? {
|
||||||
|
name: payload.game.name,
|
||||||
|
type: payload.game.type,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
status: payload.status,
|
||||||
|
afk: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { controllers } from "../api/controllers/mod.ts";
|
import { controllers } from "../api/controllers/mod.ts";
|
||||||
import { Guild } from "../api/structures/structures.ts";
|
import { Guild } from "../api/structures/structures.ts";
|
||||||
|
import { eventHandlers, IdentifyPayload } from "../bot.ts";
|
||||||
import {
|
import {
|
||||||
DiscordBotGatewayData,
|
DiscordBotGatewayData,
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
createShard,
|
createShard,
|
||||||
requestGuildMembers,
|
requestGuildMembers,
|
||||||
} from "./mod.ts";
|
} from "./mod.ts";
|
||||||
import { eventHandlers, IdentifyPayload } from "../bot.ts";
|
|
||||||
|
|
||||||
let createNextShard = true;
|
let createNextShard = true;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user