mirror of
https://github.com/discordeno/discordeno.git
synced 2026-06-16 11:28:15 +00:00
chore: verbose imports
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
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 } from "../structures/channel.ts";
|
||||||
|
import { Guild } from "../structures/guild.ts";
|
||||||
|
import { Member } from "../structures/member.ts";
|
||||||
|
import { Message } from "../structures/message.ts";
|
||||||
|
|
||||||
export type TableName =
|
export type TableName =
|
||||||
| "guilds"
|
| "guilds"
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
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 { createChannel } from "../structures/channel.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalChannelCreate(data: DiscordPayload) {
|
export async function handleInternalChannelCreate(data: DiscordPayload) {
|
||||||
if (data.t !== "CHANNEL_CREATE") return;
|
if (data.t !== "CHANNEL_CREATE") return;
|
||||||
|
|
||||||
const payload = data.d as ChannelCreatePayload;
|
const payload = data.d as ChannelCreatePayload;
|
||||||
const channel = await structures.createChannel(payload);
|
const channel = await createChannel(payload);
|
||||||
await cacheHandlers.set("channels", channel.id, channel);
|
await cacheHandlers.set("channels", channel.id, channel);
|
||||||
|
|
||||||
eventHandlers.channelCreate?.(channel);
|
eventHandlers.channelCreate?.(channel);
|
||||||
@@ -57,7 +57,7 @@ export async function handleInternalChannelUpdate(data: DiscordPayload) {
|
|||||||
|
|
||||||
const payload = data.d as ChannelCreatePayload;
|
const payload = data.d as ChannelCreatePayload;
|
||||||
const cachedChannel = await cacheHandlers.get("channels", payload.id);
|
const cachedChannel = await cacheHandlers.get("channels", payload.id);
|
||||||
const channel = await structures.createChannel(payload);
|
const channel = await createChannel(payload);
|
||||||
cacheHandlers.set("channels", channel.id, channel);
|
cacheHandlers.set("channels", channel.id, channel);
|
||||||
|
|
||||||
if (!cachedChannel) return;
|
if (!cachedChannel) return;
|
||||||
|
|||||||
@@ -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 { createGuild } from "../structures/guild.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalGuildCreate(
|
export async function handleInternalGuildCreate(
|
||||||
@@ -21,7 +21,7 @@ export async function handleInternalGuildCreate(
|
|||||||
// When shards resume they emit GUILD_CREATE again.
|
// When shards resume they emit GUILD_CREATE again.
|
||||||
if (await cacheHandlers.has("guilds", payload.id)) return;
|
if (await cacheHandlers.has("guilds", payload.id)) return;
|
||||||
|
|
||||||
const guild = await structures.createGuild(
|
const guild = await createGuild(
|
||||||
data.d as CreateGuildPayload,
|
data.d as CreateGuildPayload,
|
||||||
shardID,
|
shardID,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { DiscordPayload } from "../../types/types.ts";
|
|
||||||
import { eventHandlers } from "../../bot.ts";
|
import { eventHandlers } from "../../bot.ts";
|
||||||
import { structures } from "../structures/mod.ts";
|
import {
|
||||||
import { InteractionCommandPayload } from "../../types/types.ts";
|
DiscordPayload,
|
||||||
|
InteractionCommandPayload,
|
||||||
|
} from "../../types/types.ts";
|
||||||
|
import { createMember } from "../structures/member.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;
|
||||||
@@ -11,7 +13,7 @@ export async function handleInternalInteractionsCreate(data: DiscordPayload) {
|
|||||||
eventHandlers.interactionCreate?.(
|
eventHandlers.interactionCreate?.(
|
||||||
{
|
{
|
||||||
...payload,
|
...payload,
|
||||||
member: await structures.createMember(payload.member, payload.guild_id),
|
member: await createMember(payload.member, payload.guild_id),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { createMember } from "../structures/member.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalGuildMemberAdd(data: DiscordPayload) {
|
export async function handleInternalGuildMemberAdd(data: DiscordPayload) {
|
||||||
@@ -18,7 +18,7 @@ export async function handleInternalGuildMemberAdd(data: DiscordPayload) {
|
|||||||
if (!guild) return;
|
if (!guild) return;
|
||||||
|
|
||||||
guild.memberCount++;
|
guild.memberCount++;
|
||||||
const member = await structures.createMember(
|
const member = await createMember(
|
||||||
payload,
|
payload,
|
||||||
payload.guild_id,
|
payload.guild_id,
|
||||||
);
|
);
|
||||||
@@ -60,7 +60,7 @@ export async function handleInternalGuildMemberUpdate(data: DiscordPayload) {
|
|||||||
mute: guildMember?.mute || false,
|
mute: guildMember?.mute || false,
|
||||||
roles: payload.roles,
|
roles: payload.roles,
|
||||||
};
|
};
|
||||||
const member = await structures.createMember(
|
const member = await createMember(
|
||||||
newMemberData,
|
newMemberData,
|
||||||
payload.guild_id,
|
payload.guild_id,
|
||||||
);
|
);
|
||||||
@@ -98,7 +98,7 @@ export async function handleInternalGuildMembersChunk(data: DiscordPayload) {
|
|||||||
if (!guild) return;
|
if (!guild) return;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
payload.members.map((member) => structures.createMember(member, guild.id)),
|
payload.members.map((member) => createMember(member, guild.id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming
|
// Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
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 { createMember } from "../structures/member.ts";
|
||||||
|
import { createMessage } from "../structures/message.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalMessageCreate(data: DiscordPayload) {
|
export async function handleInternalMessageCreate(data: DiscordPayload) {
|
||||||
@@ -21,7 +22,7 @@ export async function handleInternalMessageCreate(data: DiscordPayload) {
|
|||||||
|
|
||||||
if (payload.member && guild) {
|
if (payload.member && guild) {
|
||||||
// If in a guild cache the author as a member
|
// If in a guild cache the author as a member
|
||||||
await structures.createMember(
|
await createMember(
|
||||||
{ ...payload.member, user: payload.author },
|
{ ...payload.member, user: payload.author },
|
||||||
guild.id,
|
guild.id,
|
||||||
);
|
);
|
||||||
@@ -30,14 +31,14 @@ export async function handleInternalMessageCreate(data: DiscordPayload) {
|
|||||||
payload.mentions.forEach((mention) => {
|
payload.mentions.forEach((mention) => {
|
||||||
// Cache the member if its a valid member
|
// Cache the member if its a valid member
|
||||||
if (mention.member && guild) {
|
if (mention.member && guild) {
|
||||||
structures.createMember(
|
createMember(
|
||||||
{ ...mention.member, user: mention },
|
{ ...mention.member, user: mention },
|
||||||
guild.id,
|
guild.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = await structures.createMessage(payload);
|
const message = await createMessage(payload);
|
||||||
// Cache the message
|
// Cache the message
|
||||||
cacheHandlers.set("messages", payload.id, message);
|
cacheHandlers.set("messages", payload.id, message);
|
||||||
|
|
||||||
|
|||||||
@@ -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,9 @@ 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 } from "../structures/guild.ts";
|
||||||
|
import { createMember } from "../structures/member.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalReady(
|
export async function handleInternalReady(
|
||||||
@@ -37,7 +35,7 @@ export async function handleInternalReady(
|
|||||||
// All the members that came in on guild creates should now be processed 1 by 1
|
// All the members that came in on guild creates should now be processed 1 by 1
|
||||||
for (const [guildID, members] of initialMemberLoadQueue.entries()) {
|
for (const [guildID, members] of initialMemberLoadQueue.entries()) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
members.map((member) => structures.createMember(member, guildID)),
|
members.map((member) => createMember(member, guildID)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +85,7 @@ export async function handleInternalVoiceStateUpdate(data: DiscordPayload) {
|
|||||||
if (!guild) return;
|
if (!guild) return;
|
||||||
|
|
||||||
const member = payload.member
|
const member = payload.member
|
||||||
? await structures.createMember(payload.member, guild.id)
|
? await createMember(payload.member, guild.id)
|
||||||
: await cacheHandlers.get("members", payload.user_id);
|
: await cacheHandlers.get("members", payload.user_id);
|
||||||
if (!member) return;
|
if (!member) return;
|
||||||
|
|
||||||
|
|||||||
@@ -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 { createMember } from "../structures/member.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalMessageReactionAdd(data: DiscordPayload) {
|
export async function handleInternalMessageReactionAdd(data: DiscordPayload) {
|
||||||
@@ -40,7 +40,7 @@ export async function handleInternalMessageReactionAdd(data: DiscordPayload) {
|
|||||||
if (payload.member && payload.guild_id) {
|
if (payload.member && payload.guild_id) {
|
||||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||||
if (guild) {
|
if (guild) {
|
||||||
await structures.createMember(payload.member, guild.id);
|
await createMember(payload.member, guild.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ export async function handleInternalMessageReactionRemove(
|
|||||||
if (payload.member && payload.guild_id) {
|
if (payload.member && payload.guild_id) {
|
||||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||||
if (guild) {
|
if (guild) {
|
||||||
await structures.createMember(
|
await createMember(
|
||||||
payload.member,
|
payload.member,
|
||||||
guild.id,
|
guild.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 { createRole } from "../structures/role.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalGuildRoleCreate(data: DiscordPayload) {
|
export async function handleInternalGuildRoleCreate(data: DiscordPayload) {
|
||||||
@@ -14,7 +14,7 @@ export async function handleInternalGuildRoleCreate(data: DiscordPayload) {
|
|||||||
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
const guild = await cacheHandlers.get("guilds", payload.guild_id);
|
||||||
if (!guild) return;
|
if (!guild) return;
|
||||||
|
|
||||||
const role = await structures.createRole(payload.role);
|
const role = await createRole(payload.role);
|
||||||
const roles = guild.roles.set(payload.role.id, role);
|
const roles = guild.roles.set(payload.role.id, role);
|
||||||
guild.roles = roles;
|
guild.roles = roles;
|
||||||
return eventHandlers.roleCreate?.(guild, role);
|
return eventHandlers.roleCreate?.(guild, role);
|
||||||
@@ -55,7 +55,7 @@ export async function handleInternalGuildRoleUpdate(data: DiscordPayload) {
|
|||||||
const cachedRole = guild.roles.get(payload.role.id);
|
const cachedRole = guild.roles.get(payload.role.id);
|
||||||
if (!cachedRole) return;
|
if (!cachedRole) return;
|
||||||
|
|
||||||
const role = await structures.createRole(payload.role);
|
const role = await createRole(payload.role);
|
||||||
guild.roles.set(payload.role.id, role);
|
guild.roles.set(payload.role.id, role);
|
||||||
eventHandlers.roleUpdate?.(guild, role, cachedRole);
|
eventHandlers.roleUpdate?.(guild, role, cachedRole);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { createMessage } from "../structures/message.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(
|
||||||
@@ -73,7 +73,7 @@ export async function getMessage(
|
|||||||
const result = await RequestManager.get(
|
const result = await RequestManager.get(
|
||||||
endpoints.CHANNEL_MESSAGE(channelID, id),
|
endpoints.CHANNEL_MESSAGE(channelID, id),
|
||||||
) as MessageCreateOptions;
|
) as MessageCreateOptions;
|
||||||
return structures.createMessage(result);
|
return createMessage(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetches between 2-100 messages. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */
|
/** Fetches between 2-100 messages. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */
|
||||||
@@ -111,7 +111,7 @@ export async function getMessages(
|
|||||||
endpoints.CHANNEL_MESSAGES(channelID),
|
endpoints.CHANNEL_MESSAGES(channelID),
|
||||||
options,
|
options,
|
||||||
)) as MessageCreateOptions[];
|
)) as MessageCreateOptions[];
|
||||||
return Promise.all(result.map((res) => structures.createMessage(res)));
|
return Promise.all(result.map((res) => createMessage(res)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get pinned messages in this channel. */
|
/** Get pinned messages in this channel. */
|
||||||
@@ -119,7 +119,7 @@ export async function getPins(channelID: string) {
|
|||||||
const result = (await RequestManager.get(
|
const result = (await RequestManager.get(
|
||||||
endpoints.CHANNEL_PINS(channelID),
|
endpoints.CHANNEL_PINS(channelID),
|
||||||
)) as MessageCreateOptions[];
|
)) as MessageCreateOptions[];
|
||||||
return Promise.all(result.map((res) => structures.createMessage(res)));
|
return Promise.all(result.map((res) => createMessage(res)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Send a message to the channel. Requires SEND_MESSAGES permission. */
|
/** Send a message to the channel. Requires SEND_MESSAGES permission. */
|
||||||
@@ -227,7 +227,7 @@ export async function sendMessage(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return structures.createMessage(result as MessageCreateOptions);
|
return createMessage(result as MessageCreateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Delete messages from the channel. 2-100. Requires the MANAGE_MESSAGES permission */
|
/** Delete messages from the channel. 2-100. Requires the MANAGE_MESSAGES permission */
|
||||||
|
|||||||
+18
-19
@@ -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,
|
||||||
@@ -45,6 +37,13 @@ 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 { urlToBase64 } from "../../util/utils.ts";
|
||||||
|
import { requestAllMembers } from "../../ws/shard_manager.ts";
|
||||||
|
import { cacheHandlers } from "../controllers/cache.ts";
|
||||||
|
import { createChannel } from "../structures/channel.ts";
|
||||||
|
import { createGuild, Guild } from "../structures/guild.ts";
|
||||||
|
import { createMember, Member } from "../structures/member.ts";
|
||||||
|
import { createRole } from "../structures/role.ts";
|
||||||
|
import { createTemplate, Template } from "../structures/template.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) {
|
||||||
@@ -52,7 +51,7 @@ export async function createServer(options: CreateServerOptions) {
|
|||||||
endpoints.GUILDS,
|
endpoints.GUILDS,
|
||||||
options,
|
options,
|
||||||
) as CreateGuildPayload;
|
) as CreateGuildPayload;
|
||||||
return structures.createGuild(guild, 0);
|
return createGuild(guild, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Delete a guild permanently. User must be owner. Returns 204 No Content on success. Fires a Guild Delete Gateway event.
|
/** Delete a guild permanently. User must be owner. Returns 204 No Content on success. Fires a Guild Delete Gateway event.
|
||||||
@@ -136,7 +135,7 @@ export async function createGuildChannel(
|
|||||||
type: options?.type || ChannelTypes.GUILD_TEXT,
|
type: options?.type || ChannelTypes.GUILD_TEXT,
|
||||||
})) as ChannelCreatePayload;
|
})) as ChannelCreatePayload;
|
||||||
|
|
||||||
const channel = await structures.createChannel(result);
|
const channel = await createChannel(result);
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +165,7 @@ export async function getChannels(guildID: string, addToCache = true) {
|
|||||||
endpoints.GUILD_CHANNELS(guildID),
|
endpoints.GUILD_CHANNELS(guildID),
|
||||||
) as ChannelCreatePayload[];
|
) as ChannelCreatePayload[];
|
||||||
return Promise.all(result.map(async (res) => {
|
return Promise.all(result.map(async (res) => {
|
||||||
const channel = await structures.createChannel(res, guildID);
|
const channel = await createChannel(res, guildID);
|
||||||
if (addToCache) {
|
if (addToCache) {
|
||||||
cacheHandlers.set("channels", channel.id, channel);
|
cacheHandlers.set("channels", channel.id, channel);
|
||||||
}
|
}
|
||||||
@@ -182,7 +181,7 @@ export async function getChannel(channelID: string, addToCache = true) {
|
|||||||
const result = await RequestManager.get(
|
const result = await RequestManager.get(
|
||||||
endpoints.GUILD_CHANNEL(channelID),
|
endpoints.GUILD_CHANNEL(channelID),
|
||||||
) as ChannelCreatePayload;
|
) as ChannelCreatePayload;
|
||||||
const channel = await structures.createChannel(result, result.guild_id);
|
const channel = await createChannel(result, result.guild_id);
|
||||||
if (addToCache) cacheHandlers.set("channels", channel.id, channel);
|
if (addToCache) cacheHandlers.set("channels", channel.id, channel);
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
@@ -217,7 +216,7 @@ export async function getMember(
|
|||||||
endpoints.GUILD_MEMBER(guildID, id),
|
endpoints.GUILD_MEMBER(guildID, id),
|
||||||
) as MemberCreatePayload;
|
) as MemberCreatePayload;
|
||||||
|
|
||||||
const member = await structures.createMember(data, guildID);
|
const member = await createMember(data, guildID);
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +322,7 @@ export async function createGuildRole(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const roleData = result as RoleData;
|
const roleData = result as RoleData;
|
||||||
const role = await structures.createRole(roleData);
|
const role = await createRole(roleData);
|
||||||
const guild = await cacheHandlers.get("guilds", guildID);
|
const guild = await cacheHandlers.get("guilds", guildID);
|
||||||
guild?.roles.set(role.id, role);
|
guild?.roles.set(role.id, role);
|
||||||
return role;
|
return role;
|
||||||
@@ -713,7 +712,7 @@ export async function getGuildTemplates(guildID: string) {
|
|||||||
const templates = await RequestManager.get(
|
const templates = await RequestManager.get(
|
||||||
endpoints.GUILD_TEMPLATES(guildID),
|
endpoints.GUILD_TEMPLATES(guildID),
|
||||||
) as GuildTemplate[];
|
) as GuildTemplate[];
|
||||||
return templates.map((template) => structures.createTemplate(template));
|
return templates.map((template) => createTemplate(template));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -730,7 +729,7 @@ export async function deleteGuildTemplate(
|
|||||||
const deletedTemplate = await RequestManager.delete(
|
const deletedTemplate = await RequestManager.delete(
|
||||||
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
|
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
|
||||||
) as GuildTemplate;
|
) as GuildTemplate;
|
||||||
return structures.createTemplate(deletedTemplate);
|
return createTemplate(deletedTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -761,7 +760,7 @@ export async function createGuildTemplate(
|
|||||||
endpoints.GUILD_TEMPLATES(guildID),
|
endpoints.GUILD_TEMPLATES(guildID),
|
||||||
data,
|
data,
|
||||||
) as GuildTemplate;
|
) as GuildTemplate;
|
||||||
return structures.createTemplate(template);
|
return createTemplate(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -775,7 +774,7 @@ export async function syncGuildTemplate(guildID: string, templateCode: string) {
|
|||||||
const template = await RequestManager.put(
|
const template = await RequestManager.put(
|
||||||
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
|
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
|
||||||
) as GuildTemplate;
|
) as GuildTemplate;
|
||||||
return structures.createTemplate(template);
|
return createTemplate(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -805,5 +804,5 @@ export async function editGuildTemplate(
|
|||||||
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
|
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
|
||||||
data,
|
data,
|
||||||
) as GuildTemplate;
|
) as GuildTemplate;
|
||||||
return structures.createTemplate(template);
|
return createTemplate(template);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -18,6 +16,9 @@ import {
|
|||||||
highestRole,
|
highestRole,
|
||||||
} from "../../util/permissions.ts";
|
} from "../../util/permissions.ts";
|
||||||
import { urlToBase64 } from "../../util/utils.ts";
|
import { urlToBase64 } from "../../util/utils.ts";
|
||||||
|
import { cacheHandlers } from "../controllers/cache.ts";
|
||||||
|
import { createChannel } from "../structures/channel.ts";
|
||||||
|
import { Member } from "../structures/member.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. */
|
||||||
@@ -123,7 +124,7 @@ export async function sendDirectMessage(
|
|||||||
) as DMChannelCreatePayload;
|
) as DMChannelCreatePayload;
|
||||||
// Channel create event will have added this channel to the cache
|
// Channel create event will have added this channel to the cache
|
||||||
cacheHandlers.delete("channels", dmChannelData.id);
|
cacheHandlers.delete("channels", dmChannelData.id);
|
||||||
const channel = await structures.createChannel(dmChannelData);
|
const channel = await createChannel(dmChannelData);
|
||||||
// Recreate the channel and add it undert he users id
|
// Recreate the channel and add it undert he users id
|
||||||
cacheHandlers.set("channels", memberID, channel);
|
cacheHandlers.set("channels", memberID, channel);
|
||||||
dmChannel = channel;
|
dmChannel = channel;
|
||||||
|
|||||||
@@ -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 { createMessage, Message } from "../structures/message.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(
|
||||||
@@ -274,7 +274,7 @@ export async function editMessage(
|
|||||||
endpoints.CHANNEL_MESSAGE(message.channelID, message.id),
|
endpoints.CHANNEL_MESSAGE(message.channelID, message.id),
|
||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
return structures.createMessage(result as MessageCreateOptions);
|
return createMessage(result as MessageCreateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function publishMessage(channelID: string, messageID: string) {
|
export async function publishMessage(channelID: string, messageID: string) {
|
||||||
@@ -282,5 +282,5 @@ export async function publishMessage(channelID: string, messageID: string) {
|
|||||||
endpoints.CHANNEL_MESSAGE_CROSSPOST(channelID, messageID),
|
endpoints.CHANNEL_MESSAGE_CROSSPOST(channelID, messageID),
|
||||||
) as MessageCreateOptions;
|
) as MessageCreateOptions;
|
||||||
|
|
||||||
return structures.createMessage(data);
|
return createMessage(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { createMessage } from "../structures/message.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:
|
||||||
*
|
*
|
||||||
@@ -110,7 +110,7 @@ export async function executeWebhook(
|
|||||||
);
|
);
|
||||||
if (!options.wait) return;
|
if (!options.wait) return;
|
||||||
|
|
||||||
return structures.createMessage(result as MessageCreateOptions);
|
return createMessage(result as MessageCreateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebhook(webhookID: string) {
|
export function getWebhook(webhookID: string) {
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export * from "./channel.ts";
|
|
||||||
export * from "./guild.ts";
|
|
||||||
export * from "./member.ts";
|
|
||||||
export * from "./message.ts";
|
|
||||||
export * from "./mod.ts";
|
|
||||||
export * from "./role.ts";
|
|
||||||
export * from "./template.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,418 @@
|
|||||||
|
import { Errors, HttpResponseCode, RequestMethods } from "../types/types.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;
|
||||||
|
}
|
||||||
+4
-6
@@ -1,9 +1,7 @@
|
|||||||
import {
|
import { Channel } from "../api/structures/channel.ts";
|
||||||
Channel,
|
import { Guild } from "../api/structures/guild.ts";
|
||||||
Guild,
|
import { Member } from "../api/structures/member.ts";
|
||||||
Member,
|
import { Message } from "../api/structures/message.ts";
|
||||||
Message,
|
|
||||||
} from "../api/structures/structures.ts";
|
|
||||||
import { PresenceUpdatePayload } from "../types/types.ts";
|
import { PresenceUpdatePayload } from "../types/types.ts";
|
||||||
import { Collection } from "./collection.ts";
|
import { Collection } from "./collection.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { cacheHandlers } from "../api/controllers/cache.ts";
|
import { cacheHandlers } from "../api/controllers/cache.ts";
|
||||||
|
import { Guild } from "../api/structures/guild.ts";
|
||||||
|
import { Role } from "../api/structures/role.ts";
|
||||||
import { botID } from "../bot.ts";
|
import { botID } from "../bot.ts";
|
||||||
import { Guild, Role } from "../api/structures/structures.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. */
|
||||||
|
|||||||
+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,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
+436
@@ -0,0 +1,436 @@
|
|||||||
|
import { botGatewayData, eventHandlers } from "../bot.ts";
|
||||||
|
import {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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/guild.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