mirror of
https://github.com/discordeno/discordeno.git
synced 2026-06-02 17:00:08 +00:00
Merge branch 'main' of https://github.com/discordeno/discordeno into main
This commit is contained in:
@@ -5,13 +5,14 @@ import { DiscordGatewayIntents } from "./types/gateway/gateway_intents.ts";
|
||||
import { snowflakeToBigint } from "./util/bigint.ts";
|
||||
import { GATEWAY_VERSION } from "./util/constants.ts";
|
||||
import { ws } from "./ws/ws.ts";
|
||||
import { dispatchRequirements } from "./util/dispatch_requirements.ts";
|
||||
|
||||
// deno-lint-ignore prefer-const
|
||||
export let secretKey = "";
|
||||
export let botId = 0n;
|
||||
export let applicationId = 0n;
|
||||
|
||||
export let eventHandlers: EventHandlers = {};
|
||||
export let eventHandlers: EventHandlers = { dispatchRequirements };
|
||||
|
||||
export let proxyWSURL = `wss://gateway.discord.gg`;
|
||||
|
||||
|
||||
44
src/cache.ts
44
src/cache.ts
@@ -1,4 +1,5 @@
|
||||
// deno-lint-ignore-file require-await no-explicit-any prefer-const
|
||||
import { botId } from "./bot.ts";
|
||||
import type { DiscordenoChannel } from "./structures/channel.ts";
|
||||
import type { DiscordenoGuild } from "./structures/guild.ts";
|
||||
import type { DiscordenoMember } from "./structures/member.ts";
|
||||
@@ -10,17 +11,17 @@ import { Collection } from "./util/collection.ts";
|
||||
export const cache = {
|
||||
isReady: false,
|
||||
/** All of the guild objects the bot has access to, mapped by their Ids */
|
||||
guilds: new Collection<bigint, DiscordenoGuild>(),
|
||||
guilds: new Collection<bigint, DiscordenoGuild>([], { sweeper: { filter: guildSweeper, interval: 3600000 } }),
|
||||
/** All of the channel objects the bot has access to, mapped by their Ids */
|
||||
channels: new Collection<bigint, DiscordenoChannel>(),
|
||||
/** All of the message objects the bot has cached since the bot acquired `READY` state, mapped by their Ids */
|
||||
messages: new Collection<bigint, DiscordenoMessage>(),
|
||||
messages: new Collection<bigint, DiscordenoMessage>([], { sweeper: { filter: messageSweeper, interval: 300000 } }),
|
||||
/** All of the member objects that have been cached since the bot acquired `READY` state, mapped by their Ids */
|
||||
members: new Collection<bigint, DiscordenoMember>(),
|
||||
members: new Collection<bigint, DiscordenoMember>([], { sweeper: { filter: memberSweeper, interval: 300000 } }),
|
||||
/** All of the unavailable guilds, mapped by their Ids (id, timestamp) */
|
||||
unavailableGuilds: new Collection<bigint, number>(),
|
||||
/** All of the presence update objects received in PRESENCE_UPDATE gateway event, mapped by their user Id */
|
||||
presences: new Collection<bigint, PresenceUpdate>(),
|
||||
presences: new Collection<bigint, PresenceUpdate>([], { sweeper: { filter: () => true, interval: 300000 } }),
|
||||
fetchAllMembersProcessingRequests: new Collection<
|
||||
string,
|
||||
(value: Collection<bigint, DiscordenoMember> | PromiseLike<Collection<bigint, DiscordenoMember>>) => void
|
||||
@@ -31,8 +32,43 @@ export const cache = {
|
||||
this.guilds.reduce((a, b) => [...a, ...b.emojis.map((e) => [e.id, e])], [] as any[])
|
||||
);
|
||||
},
|
||||
activeGuildIds: new Set<bigint>(),
|
||||
dispatchedGuildIds: new Set<bigint>(),
|
||||
dispatchedChannelIds: new Set<bigint>(),
|
||||
};
|
||||
|
||||
function messageSweeper(message: DiscordenoMessage) {
|
||||
// DM messages aren't needed
|
||||
if (!message.guildId) return true;
|
||||
|
||||
// Only delete messages older than 10 minutes
|
||||
return Date.now() - message.timestamp > 600000;
|
||||
}
|
||||
|
||||
function memberSweeper(member: DiscordenoMember) {
|
||||
// Don't sweep the bot else strange things will happen
|
||||
if (member.id === botId) return false;
|
||||
|
||||
// Only sweep members who were not active the last 30 minutes
|
||||
return member.cachedAt - Date.now() < 1800000;
|
||||
}
|
||||
|
||||
function guildSweeper(guild: DiscordenoGuild) {
|
||||
// Reset activity for next interval
|
||||
if (!cache.activeGuildIds.delete(guild.id)) return false;
|
||||
|
||||
guild.channels.forEach((channel) => {
|
||||
cache.channels.delete(channel.id);
|
||||
cache.dispatchedChannelIds.add(channel.id);
|
||||
});
|
||||
|
||||
// This is inactive guild. Not a single thing has happened for atleast 30 minutes.
|
||||
// Not a reaction, not a message, not any event!
|
||||
cache.dispatchedGuildIds.add(guild.id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export let cacheHandlers = {
|
||||
/** Deletes all items from the cache */
|
||||
async clear(table: TableName) {
|
||||
|
||||
@@ -38,7 +38,7 @@ function checkReady(payload: Ready, shard: DiscordenoShard) {
|
||||
// Check if all guilds were loaded
|
||||
if (!shard.unavailableGuildIds.size) return loaded(shard);
|
||||
|
||||
// If the last GUILD_CREATE has been received before 5 seconds if so most likely the remaining guilds are unavailable
|
||||
// If the last GUILD_CREATE was received 5 seconds ago, the remaining guilds are most likely not available
|
||||
if (shard.lastAvailable + 5000 < Date.now()) {
|
||||
eventHandlers.shardFailedToLoad?.(shard.id, shard.unavailableGuildIds);
|
||||
// Force execute the loaded function to prevent infinite loop
|
||||
|
||||
@@ -114,8 +114,9 @@ import { getGuildTemplates } from "./templates/get_guild_templates.ts";
|
||||
import { getTemplate } from "./templates/get_template.ts";
|
||||
import { syncGuildTemplate } from "./templates/sync_guild_template.ts";
|
||||
// Type Guards
|
||||
import { isActionRow } from "./type_guards/is_action_row.ts";
|
||||
import { isButton } from "./type_guards/is_button.ts";
|
||||
import { isSelectMenu } from "./type_guards/is_select_menu.ts";
|
||||
|
||||
import { createWebhook } from "./webhooks/create_webhook.ts";
|
||||
import { deleteWebhook } from "./webhooks/delete_webhook.ts";
|
||||
import { deleteWebhookMessage } from "./webhooks/delete_webhook_message.ts";
|
||||
@@ -239,8 +240,8 @@ export {
|
||||
guildBannerURL,
|
||||
guildIconURL,
|
||||
guildSplashURL,
|
||||
isActionRow,
|
||||
isButton,
|
||||
isSelectMenu,
|
||||
isChannelSynced,
|
||||
kick,
|
||||
kickMember,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { ActionRow } from "../../types/messages/components/action_row.ts";
|
||||
import type { MessageComponent } from "../../types/messages/components/message_components.ts";
|
||||
import { MessageComponentTypes } from "../../types/messages/components/message_component_types.ts";
|
||||
|
||||
/** A type guard function to tell if it is a action row component */
|
||||
export function isActionRow(component: MessageComponent): component is ActionRow {
|
||||
return component.type === MessageComponentTypes.ActionRow;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ButtonComponent } from "../../types/messages/components/button_component.ts";
|
||||
import type { MessageComponent } from "../../types/messages/components/message_components.ts";
|
||||
import type { ActionRoleComponents } from "../../types/messages/components/message_components.ts";
|
||||
import { MessageComponentTypes } from "../../types/messages/components/message_component_types.ts";
|
||||
|
||||
/** A type guard function to tell if it is a button component */
|
||||
export function isButton(component: MessageComponent): component is ButtonComponent {
|
||||
export function isButton(component: ActionRoleComponents): component is ButtonComponent {
|
||||
return component.type === MessageComponentTypes.Button;
|
||||
}
|
||||
|
||||
8
src/helpers/type_guards/is_select_menu.ts
Normal file
8
src/helpers/type_guards/is_select_menu.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { ActionRoleComponents } from "../../types/messages/components/message_components.ts";
|
||||
import { MessageComponentTypes } from "../../types/messages/components/message_component_types.ts";
|
||||
import type { SelectMenuComponent } from "../../types/messages/components/select_menu.ts";
|
||||
|
||||
/** A type guard function to tell if it is a button component */
|
||||
export function isSelectMenu(component: ActionRoleComponents): component is SelectMenuComponent {
|
||||
return component.type === MessageComponentTypes.SelectMenu;
|
||||
}
|
||||
@@ -44,8 +44,13 @@ export async function processQueue(id: string) {
|
||||
// IF THIS IS A GET REQUEST, CHANGE THE BODY TO QUERY PARAMETERS
|
||||
const query =
|
||||
queuedRequest.request.method.toUpperCase() === "GET" && queuedRequest.payload.body
|
||||
? Object.entries(queuedRequest.payload.body)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value as string)}`)
|
||||
? Object.keys(queuedRequest.payload.body)
|
||||
.map(
|
||||
(key) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(
|
||||
(queuedRequest.payload.body as Record<string, string>)[key]
|
||||
)}`
|
||||
)
|
||||
.join("&")
|
||||
: "";
|
||||
const urlToUse =
|
||||
|
||||
@@ -169,6 +169,7 @@ export async function createDiscordenoMember(
|
||||
/** The guild related data mapped by guild id */
|
||||
guilds: createNewProp(new Collection<bigint, GuildMember>()),
|
||||
bitfield: createNewProp(bitfield),
|
||||
cachedAt: createNewProp(Date.now()),
|
||||
});
|
||||
|
||||
const cached = await cacheHandlers.get("members", snowflakeToBigint(user.id));
|
||||
@@ -210,6 +211,8 @@ export interface DiscordenoMember extends Omit<User, "discriminator" | "id" | "a
|
||||
>;
|
||||
/** Holds all the boolean toggles. */
|
||||
bitfield: bigint;
|
||||
/** When the member has been cached the last time. */
|
||||
cachedAt: number;
|
||||
|
||||
// GETTERS
|
||||
/** The avatar url using the default format and size. */
|
||||
@@ -230,7 +233,9 @@ export interface DiscordenoMember extends Omit<User, "discriminator" | "id" | "a
|
||||
/** Get the nickname or the username if no nickname */
|
||||
name(guildId: bigint): string;
|
||||
/** Get the guild member object for the specified guild */
|
||||
guildMember(guildId: bigint):
|
||||
guildMember(
|
||||
guildId: bigint
|
||||
):
|
||||
| (Omit<GuildMember, "joinedAt" | "premiumSince" | "roles"> & {
|
||||
joinedAt?: number;
|
||||
premiumSince?: number;
|
||||
|
||||
@@ -98,4 +98,16 @@ export enum Errors {
|
||||
COMPONENT_LABEL_TOO_BIG = "COMPONENT_LABEL_TOO_BIG",
|
||||
COMPONENT_CUSTOM_ID_TOO_BIG = "COMPONENT_CUSTOM_ID_TOO_BIG",
|
||||
BUTTON_REQUIRES_CUSTOM_ID = "BUTTON_REQUIRES_CUSTOM_ID",
|
||||
COMPONENT_SELECT_MUST_BE_ALONE = "COMPONENT_SELECT_MUST_BE_ALONE",
|
||||
COMPONENT_PLACEHOLDER_TOO_BIG = "COMPONENT_PLACEHOLDER_TOO_BIG",
|
||||
COMPONENT_SELECT_MINVALUE_TOO_LOW = "COMPONENT_SELECT_MINVALUE_TOO_LOW",
|
||||
COMPONENT_SELECT_MINVALUE_TOO_MANY = "COMPONENT_SELECT_MINVALUE_TOO_MANY",
|
||||
COMPONENT_SELECT_MAXVALUE_TOO_LOW = "COMPONENT_SELECT_MAXVALUE_TOO_LOW",
|
||||
COMPONENT_SELECT_MAXVALUE_TOO_MANY = "COMPONENT_SELECT_MAXVALUE_TOO_MANY",
|
||||
COMPONENT_SELECT_OPTIONS_TOO_LOW = "COMPONENT_SELECT_OPTIONS_TOO_LOW",
|
||||
COMPONENT_SELECT_OPTIONS_TOO_MANY = "COMPONENT_SELECT_OPTIONS_TOO_MANY",
|
||||
SELECT_OPTION_LABEL_TOO_BIG = "SELECT_OPTION_LABEL_TOO_BIG",
|
||||
SELECT_OPTION_VALUE_TOO_BIG = "SELECT_OPTION_VALUE_TOO_BIG",
|
||||
SELECT_OPTION_TOO_MANY_DEFAULTS = "SELECT_OPTION_TOO_MANY_DEFAULTS",
|
||||
COMPONENT_SELECT_MIN_HIGHER_THAN_MAX = "COMPONENT_SELECT_MIN_HIGHER_THAN_MAX",
|
||||
}
|
||||
|
||||
@@ -11,8 +11,4 @@ export interface ApplicationCommandInteractionData {
|
||||
resolved?: ApplicationCommandInteractionDataResolved;
|
||||
/** The params + values from the user */
|
||||
options?: ApplicationCommandInteractionDataOption[];
|
||||
/** with the value you defined for this component */
|
||||
customId?: string;
|
||||
/** The type of this component */
|
||||
componentType?: 2;
|
||||
}
|
||||
|
||||
@@ -3,17 +3,32 @@ import { User } from "../users/user.ts";
|
||||
import { ApplicationCommandInteractionData } from "./commands/application_command_interaction_data.ts";
|
||||
import { InteractionGuildMember } from "./interaction_guild_member.ts";
|
||||
import { DiscordInteractionTypes } from "./interaction_types.ts";
|
||||
import { SelectMenuData } from "../messages/components/select_data.ts";
|
||||
import { ButtonData } from "../messages/components/button_data.ts";
|
||||
|
||||
/** https://discord.com/developers/docs/interactions/slash-commands#interaction */
|
||||
export interface Interaction {
|
||||
export interface Interaction extends BaseInteraction {
|
||||
/** The command data payload */
|
||||
data?: ApplicationCommandInteractionData | ButtonData | SelectMenuData;
|
||||
}
|
||||
|
||||
export interface SlashCommandInteraction extends BaseInteraction {
|
||||
type: DiscordInteractionTypes.ApplicationCommand;
|
||||
data?: ApplicationCommandInteractionData;
|
||||
}
|
||||
|
||||
export interface ComponentInteraction extends BaseInteraction {
|
||||
type: DiscordInteractionTypes.MessageComponent;
|
||||
data?: ButtonData | SelectMenuData;
|
||||
}
|
||||
|
||||
export interface BaseInteraction {
|
||||
/** Id of the interaction */
|
||||
id: string;
|
||||
/** Id of the application this interaction is for */
|
||||
applicationId: string;
|
||||
/** The type of interaction */
|
||||
type: DiscordInteractionTypes;
|
||||
/** The command data payload */
|
||||
data?: ApplicationCommandInteractionData;
|
||||
/** The guild it was sent from */
|
||||
guildId?: string;
|
||||
/** The channel it was sent from */
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { ButtonComponent } from "./button_component.ts";
|
||||
import { SelectMenuComponent } from "./select_menu.ts";
|
||||
|
||||
// TODO: add docs link
|
||||
export interface ActionRow {
|
||||
/** Action rows are a group of buttons. */
|
||||
type: 1;
|
||||
/** The button components */
|
||||
components: ButtonComponent[];
|
||||
/** The components in this row */
|
||||
components:
|
||||
| [SelectMenuComponent | ButtonComponent]
|
||||
| [ButtonComponent, ButtonComponent]
|
||||
| [ButtonComponent, ButtonComponent, ButtonComponent]
|
||||
| [ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent]
|
||||
| [ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent];
|
||||
}
|
||||
|
||||
6
src/types/messages/components/button_data.ts
Normal file
6
src/types/messages/components/button_data.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ButtonData {
|
||||
/** with the value you defined for this component */
|
||||
customId: string;
|
||||
/** The type of this component */
|
||||
componentType: 2;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ export enum DiscordMessageComponentTypes {
|
||||
ActionRow = 1,
|
||||
/** A button! */
|
||||
Button,
|
||||
/** A select menu. */
|
||||
SelectMenu,
|
||||
}
|
||||
|
||||
export type MessageComponentTypes = DiscordMessageComponentTypes;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ActionRow } from "./action_row.ts";
|
||||
import { ButtonComponent } from "./button_component.ts";
|
||||
import { SelectMenuComponent } from "./select_menu.ts";
|
||||
|
||||
export type MessageComponent = ActionRow | ButtonComponent;
|
||||
export type ActionRoleComponents = ButtonComponent | SelectMenuComponent;
|
||||
|
||||
export type MessageComponents = MessageComponent[];
|
||||
export type MessageComponents = ActionRow[];
|
||||
|
||||
10
src/types/messages/components/select_data.ts
Normal file
10
src/types/messages/components/select_data.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { DiscordMessageComponentTypes } from "./message_component_types.ts";
|
||||
|
||||
export interface SelectMenuData {
|
||||
/** The type of component */
|
||||
componentType: DiscordMessageComponentTypes.SelectMenu;
|
||||
/** The custom id provided for this component. */
|
||||
customId: string;
|
||||
/** The values chosen by the user. */
|
||||
values: string[];
|
||||
}
|
||||
16
src/types/messages/components/select_menu.ts
Normal file
16
src/types/messages/components/select_menu.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { DiscordMessageComponentTypes } from "./message_component_types.ts";
|
||||
import { SelectOption } from "./select_option.ts";
|
||||
|
||||
export interface SelectMenuComponent {
|
||||
type: DiscordMessageComponentTypes.SelectMenu;
|
||||
/** A custom identifier for this component. Maximum 100 characters. */
|
||||
customId: string;
|
||||
/** A custom placeholder text if nothing is selected. Maximum 100 characters. */
|
||||
placeholder?: string;
|
||||
/** The minimum number of items that must be selected. Default 1. Between 1-25. */
|
||||
minValues?: number;
|
||||
/** The maximum number of items that can be selected. Default 1. Between 1-25. */
|
||||
maxValues?: number;
|
||||
/** The choices! Maximum of 25 items. */
|
||||
options: SelectOption[];
|
||||
}
|
||||
21
src/types/messages/components/select_option.ts
Normal file
21
src/types/messages/components/select_option.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface SelectOption {
|
||||
/** The user-facing name of the option. Maximum 25 characters. */
|
||||
label: string;
|
||||
/** The dev-defined value of the option. Maximum 100 characters. */
|
||||
value: string;
|
||||
/** An additional description of the option. Maximum 50 characters. */
|
||||
description?: string;
|
||||
/** The id, name, and animated properties of an emoji. */
|
||||
emoji?:
|
||||
| string
|
||||
| {
|
||||
/** Emoji id */
|
||||
id?: string;
|
||||
/** Emoji name */
|
||||
name?: string;
|
||||
/** Whether this emoji is animated */
|
||||
animated?: boolean;
|
||||
};
|
||||
/** Will render this option as already-selected by default. */
|
||||
default: boolean;
|
||||
}
|
||||
@@ -6,6 +6,9 @@ export * from "./components/button_component.ts";
|
||||
export * from "./components/button_styles.ts";
|
||||
export * from "./components/message_component_types.ts";
|
||||
export * from "./components/message_components.ts";
|
||||
export * from "./components/select_data.ts";
|
||||
export * from "./components/select_menu.ts";
|
||||
export * from "./components/select_option.ts";
|
||||
export * from "./create_message.ts";
|
||||
export * from "./edit_message.ts";
|
||||
export * from "./get_messages.ts";
|
||||
|
||||
@@ -31,6 +31,18 @@ export class Collection<K, V> extends Map<K, V> {
|
||||
return clearInterval(this.sweeper?.intervalId);
|
||||
}
|
||||
|
||||
changeSweeperInterval(newInterval: number) {
|
||||
if (!this.sweeper) return;
|
||||
|
||||
this.startSweeper({ filter: this.sweeper.filter, interval: newInterval });
|
||||
}
|
||||
|
||||
changeSweeperFilter(newFilter: (value: V, key: K) => boolean | Promise<boolean>) {
|
||||
if (!this.sweeper) return;
|
||||
|
||||
this.startSweeper({ filter: newFilter, interval: this.sweeper.interval });
|
||||
}
|
||||
|
||||
set(key: K, value: V) {
|
||||
// When this collection is maxSizeed make sure we can add first
|
||||
if ((this.maxSize || this.maxSize === 0) && this.size >= this.maxSize) {
|
||||
|
||||
93
src/util/dispatch_requirements.ts
Normal file
93
src/util/dispatch_requirements.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { botId } from "../bot.ts";
|
||||
import { cache } from "../cache.ts";
|
||||
import { getChannels } from "../helpers/channels/get_channels.ts";
|
||||
import { getGuild } from "../helpers/guilds/get_guild.ts";
|
||||
import { getMember } from "../helpers/members/get_member.ts";
|
||||
import { structures } from "../structures/mod.ts";
|
||||
import type { DiscordGatewayPayload } from "../types/gateway/gateway_payload.ts";
|
||||
import type { Guild } from "../types/guilds/guild.ts";
|
||||
import { snowflakeToBigint } from "./bigint.ts";
|
||||
import { delay } from "./utils.ts";
|
||||
|
||||
const processing = new Set<bigint>();
|
||||
|
||||
export async function dispatchRequirements(data: DiscordGatewayPayload, shardId: number) {
|
||||
if (!cache.isReady) return;
|
||||
|
||||
// DELETE MEANS WE DONT NEED TO FETCH. CREATE SHOULD HAVE DATA TO CACHE
|
||||
if (data.t && ["GUILD_CREATE", "GUILD_DELETE"].includes(data.t)) return;
|
||||
|
||||
const id = snowflakeToBigint(
|
||||
(data.t && ["GUILD_UPDATE"].includes(data.t)
|
||||
? // deno-lint-ignore no-explicit-any
|
||||
(data.d as any)?.id
|
||||
: // deno-lint-ignore no-explicit-any
|
||||
(data.d as any)?.guild_id) ?? ""
|
||||
);
|
||||
|
||||
if (!id || cache.activeGuildIds.has(id)) return;
|
||||
|
||||
// If this guild is in cache, it has not been swept and we can cancel
|
||||
if (cache.guilds.has(id)) {
|
||||
cache.activeGuildIds.add(id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (processing.has(id)) {
|
||||
console.info(`[DISPATCH] New Guild ID already being processed: ${id} in ${data.t} event`);
|
||||
|
||||
let runs = 0;
|
||||
do {
|
||||
await delay(500);
|
||||
runs++;
|
||||
} while (processing.has(id) && runs < 40);
|
||||
|
||||
if (!processing.has(id)) return;
|
||||
|
||||
return console.warn(`[DISPATCH] Already processed guild was not successfully fetched: ${id} in ${data.t} event`);
|
||||
}
|
||||
|
||||
processing.add(id);
|
||||
|
||||
// New guild id has appeared, fetch all relevant data
|
||||
console.info(`[DISPATCH] New Guild ID has appeared: ${id} in ${data.t} event`);
|
||||
|
||||
const rawGuild = (await getGuild(id, {
|
||||
counts: true,
|
||||
addToCache: false,
|
||||
}).catch(console.info)) as Guild | undefined;
|
||||
|
||||
if (!rawGuild) {
|
||||
processing.delete(id);
|
||||
return console.warn(`[DISPATCH] Guild ID ${id} failed to fetch.`);
|
||||
}
|
||||
|
||||
console.info(`[DISPATCH] Guild ID ${id} has been found. ${rawGuild.name}`);
|
||||
|
||||
const [channels, botMember] = await Promise.all([
|
||||
getChannels(id, false),
|
||||
getMember(id, botId, { force: true }),
|
||||
]).catch((error) => {
|
||||
console.warn(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
if (!botMember || !channels) {
|
||||
processing.delete(id);
|
||||
return console.info(`[DISPATCH] Guild ID ${id} Name: ${rawGuild.name} failed. Unable to get botMember or channels`);
|
||||
}
|
||||
|
||||
const guild = await structures.createDiscordenoGuild(rawGuild, shardId);
|
||||
|
||||
// Add to cache
|
||||
cache.guilds.set(id, guild);
|
||||
cache.dispatchedGuildIds.delete(id);
|
||||
channels.forEach((channel) => {
|
||||
cache.dispatchedChannelIds.delete(channel.id);
|
||||
cache.channels.set(channel.id, channel);
|
||||
});
|
||||
|
||||
processing.delete(id);
|
||||
|
||||
console.info(`[DISPATCH] Guild ID ${id} Name: ${guild.name} completely loaded.`);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { encode } from "./deps.ts";
|
||||
import { eventHandlers } from "../bot.ts";
|
||||
import { isActionRow } from "../helpers/type_guards/is_action_row.ts";
|
||||
import { isButton } from "../helpers/type_guards/is_button.ts";
|
||||
import { Errors } from "../types/discordeno/errors.ts";
|
||||
import type { ApplicationCommandOption } from "../types/interactions/commands/application_command_option.ts";
|
||||
@@ -14,6 +13,7 @@ import type { DiscordImageFormat } from "../types/misc/image_format.ts";
|
||||
import type { DiscordImageSize } from "../types/misc/image_size.ts";
|
||||
import { SLASH_COMMANDS_NAME_REGEX } from "./constants.ts";
|
||||
import { validateLength } from "./validate_length.ts";
|
||||
import { isSelectMenu } from "../helpers/type_guards/is_select_menu.ts";
|
||||
|
||||
export async function urlToBase64(url: string) {
|
||||
const buffer = await fetch(url).then((res) => res.arrayBuffer());
|
||||
@@ -215,43 +215,6 @@ export function validateComponents(components: MessageComponents) {
|
||||
let actionRowCounter = 0;
|
||||
|
||||
for (const component of components) {
|
||||
// 5 Link buttons can not have a customId
|
||||
if (isButton(component)) {
|
||||
if (component.type === ButtonStyles.Link && component.customId) {
|
||||
throw new Error(Errors.LINK_BUTTON_CANNOT_HAVE_CUSTOM_ID);
|
||||
}
|
||||
// Other buttons must have a customId
|
||||
if (!component.customId && component.type !== ButtonStyles.Link) {
|
||||
throw new Error(Errors.BUTTON_REQUIRES_CUSTOM_ID);
|
||||
}
|
||||
|
||||
if (!validateLength(component.label, { max: 80 })) {
|
||||
throw new Error(Errors.COMPONENT_LABEL_TOO_BIG);
|
||||
}
|
||||
|
||||
if (component.customId && !validateLength(component.customId, { max: 100 })) {
|
||||
throw new Error(Errors.COMPONENT_CUSTOM_ID_TOO_BIG);
|
||||
}
|
||||
|
||||
if (typeof component.emoji === "string") {
|
||||
// A snowflake id was provided
|
||||
if (/^[0-9]+$/.test(component.emoji)) {
|
||||
component.emoji = {
|
||||
id: component.emoji,
|
||||
};
|
||||
} else {
|
||||
// A unicode emoji was provided
|
||||
component.emoji = {
|
||||
name: component.emoji,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isActionRow(component)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
actionRowCounter++;
|
||||
// Max of 5 ActionRows per message
|
||||
if (actionRowCounter > 5) throw new Error(Errors.TOO_MANY_ACTION_ROWS);
|
||||
@@ -259,6 +222,124 @@ export function validateComponents(components: MessageComponents) {
|
||||
// Max of 5 Buttons (or any component type) within an ActionRow
|
||||
if (component.components?.length > 5) {
|
||||
throw new Error(Errors.TOO_MANY_COMPONENTS);
|
||||
} else if (
|
||||
component.components?.length > 1 &&
|
||||
component.components.some((subcomponent) => isSelectMenu(subcomponent))
|
||||
) {
|
||||
throw new Error(Errors.COMPONENT_SELECT_MUST_BE_ALONE);
|
||||
}
|
||||
|
||||
for (const subcomponent of component.components) {
|
||||
if (subcomponent.customId && !validateLength(subcomponent.customId, { max: 100 })) {
|
||||
throw new Error(Errors.COMPONENT_CUSTOM_ID_TOO_BIG);
|
||||
}
|
||||
|
||||
// 5 Link buttons can not have a customId
|
||||
if (isButton(subcomponent)) {
|
||||
if (subcomponent.type === ButtonStyles.Link && subcomponent.customId) {
|
||||
throw new Error(Errors.LINK_BUTTON_CANNOT_HAVE_CUSTOM_ID);
|
||||
}
|
||||
// Other buttons must have a customId
|
||||
if (!subcomponent.customId && subcomponent.type !== ButtonStyles.Link) {
|
||||
throw new Error(Errors.BUTTON_REQUIRES_CUSTOM_ID);
|
||||
}
|
||||
|
||||
if (!validateLength(subcomponent.label, { max: 80 })) {
|
||||
throw new Error(Errors.COMPONENT_LABEL_TOO_BIG);
|
||||
}
|
||||
|
||||
subcomponent.emoji = makeEmojiFromString(subcomponent.emoji);
|
||||
}
|
||||
|
||||
if (isSelectMenu(subcomponent)) {
|
||||
if (subcomponent.placeholder && !validateLength(subcomponent.placeholder, { max: 100 })) {
|
||||
throw new Error(Errors.COMPONENT_PLACEHOLDER_TOO_BIG);
|
||||
}
|
||||
|
||||
if (subcomponent.minValues) {
|
||||
if (subcomponent.minValues < 1) {
|
||||
throw new Error(Errors.COMPONENT_SELECT_MINVALUE_TOO_LOW);
|
||||
}
|
||||
|
||||
if (subcomponent.minValues > 25) {
|
||||
throw new Error(Errors.COMPONENT_SELECT_MINVALUE_TOO_MANY);
|
||||
}
|
||||
|
||||
if (!subcomponent.maxValues) subcomponent.maxValues = subcomponent.minValues;
|
||||
if (subcomponent.minValues > subcomponent.maxValues) {
|
||||
throw new Error(Errors.COMPONENT_SELECT_MIN_HIGHER_THAN_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
if (subcomponent.maxValues) {
|
||||
if (subcomponent.maxValues < 1) {
|
||||
throw new Error(Errors.COMPONENT_SELECT_MAXVALUE_TOO_LOW);
|
||||
}
|
||||
|
||||
if (subcomponent.maxValues > 25) {
|
||||
throw new Error(Errors.COMPONENT_SELECT_MAXVALUE_TOO_MANY);
|
||||
}
|
||||
}
|
||||
|
||||
if (subcomponent.options.length < 1) {
|
||||
throw new Error(Errors.COMPONENT_SELECT_OPTIONS_TOO_LOW);
|
||||
}
|
||||
|
||||
if (subcomponent.options.length > 25) {
|
||||
throw new Error(Errors.COMPONENT_SELECT_OPTIONS_TOO_MANY);
|
||||
}
|
||||
|
||||
let defaults = 0;
|
||||
|
||||
for (const option of subcomponent.options) {
|
||||
if (option.default) {
|
||||
defaults++;
|
||||
if (defaults > (subcomponent.maxValues || 25)) {
|
||||
throw new Error(Errors.SELECT_OPTION_TOO_MANY_DEFAULTS);
|
||||
}
|
||||
}
|
||||
|
||||
if (!validateLength(option.label, { max: 25 })) {
|
||||
throw new Error(Errors.SELECT_OPTION_LABEL_TOO_BIG);
|
||||
}
|
||||
|
||||
if (!validateLength(option.value, { max: 100 })) {
|
||||
throw new Error(Errors.SELECT_OPTION_VALUE_TOO_BIG);
|
||||
}
|
||||
|
||||
if (option.description && !validateLength(option.description, { max: 50 })) {
|
||||
throw new Error(Errors.SELECT_OPTION_VALUE_TOO_BIG);
|
||||
}
|
||||
|
||||
option.emoji = makeEmojiFromString(option.emoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeEmojiFromString(
|
||||
emoji?:
|
||||
| string
|
||||
| {
|
||||
id?: string | undefined;
|
||||
name?: string | undefined;
|
||||
animated?: boolean | undefined;
|
||||
}
|
||||
) {
|
||||
if (typeof emoji !== "string") return emoji;
|
||||
|
||||
// A snowflake id was provided
|
||||
if (/^[0-9]+$/.test(emoji)) {
|
||||
emoji = {
|
||||
id: emoji,
|
||||
};
|
||||
} else {
|
||||
// A unicode emoji was provided
|
||||
emoji = {
|
||||
name: emoji,
|
||||
};
|
||||
}
|
||||
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
// THE ORDER OF THE IMPORTS IN THIS FILE MATTER!
|
||||
// DO NOT MOVE THEM UNLESS YOU KNOW WHAT YOUR DOING!
|
||||
|
||||
import "./util/utils.ts";
|
||||
import "./util/validate_length.ts";
|
||||
import "./util/loop_object.ts";
|
||||
|
||||
// Final cleanup
|
||||
|
||||
import { cache } from "../src/cache.ts";
|
||||
import { delay } from "../src/util/utils.ts";
|
||||
if (import.meta.main) {
|
||||
// clear all the sweeper intervals
|
||||
for (const c of Object.values(cache)) {
|
||||
if (!(c instanceof Map)) continue;
|
||||
|
||||
c.stopSweeper();
|
||||
console.log("Cleaned");
|
||||
}
|
||||
|
||||
await delay(3000);
|
||||
}
|
||||
|
||||
14
tests/mod.ts
14
tests/mod.ts
@@ -74,3 +74,17 @@ import "./discoveries/valid_discovery_term.ts";
|
||||
// Final cleanup
|
||||
import "./guilds/delete_guild.ts";
|
||||
import "./ws/ws_close.ts";
|
||||
|
||||
import { cache } from "../src/cache.ts";
|
||||
import { delay } from "../src/util/utils.ts";
|
||||
if (import.meta.main) {
|
||||
// clear all the sweeper intervals
|
||||
for (const c of Object.values(cache)) {
|
||||
if (!(c instanceof Map)) continue;
|
||||
|
||||
c.stopSweeper();
|
||||
console.log("Cleaned");
|
||||
}
|
||||
|
||||
await delay(3000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user