diff --git a/src/bot.ts b/src/bot.ts index 624e003e2..47a3b7ecc 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -12,7 +12,7 @@ import { DiscordGatewayIntents } from "./types/gateway/gateway_intents.ts"; import { GetGatewayBot } from "./types/gateway/get_gateway_bot.ts"; import { dispatchRequirements } from "./util/dispatch_requirements.ts"; import { processQueue } from "./rest/process_queue.ts"; -import { snowflakeToBigint } from "./util/bigint.ts"; +import { bigintToSnowflake, snowflakeToBigint } from "./util/bigint.ts"; import { Collection } from "./util/collection.ts"; import { DiscordenoUser, transformMember, transformUser } from "./transformers/member.ts"; import { SnakeCasedPropertiesDeep } from "./types/util.ts"; @@ -36,31 +36,50 @@ import { handleOnMessage } from "./ws/handle_on_message.ts"; import { closeWS } from "./ws/close_ws.ts"; import { sendShardMessage } from "./ws/send_shard_message.ts"; import { resume } from "./ws/resume.ts"; +import { calculateShardId } from "./util/calculate_shard_id.ts"; +import { + baseEndpoints, + CHANNEL_MENTION_REGEX, + CONTEXT_MENU_COMMANDS_NAME_REGEX, + DISCORDENO_VERSION, + DISCORD_SNOWFLAKE_REGEX, + endpoints, + SLASH_COMMANDS_NAME_REGEX, + USER_AGENT, +} from "./util/constants.ts"; +import { GatewayPayload } from "./types/gateway/gateway_payload.ts"; +import { delay } from "./util/utils.ts"; +import { iconBigintToHash, iconHashToBigInt } from "./util/hash.ts"; +import { loopObject } from "./util/loop_object.ts"; export async function createBot(options: CreateBotOptions) { return { id: options.botId, applicationId: options.applicationId || options.botId, token: `Bot ${options.token}`, - events: { dispatchRequirements: dispatchRequirements, ...options.events }, + events: options.events, intents: options.intents.reduce((bits, next) => (bits |= DiscordGatewayIntents[next]), 0), botGatewayData: options.botGatewayData || (await getGatewayBot()), isReady: false, + activeGuildIds: new Set(), + constants: createBotConstants(), }; } const bot = await createBot({ token: "", botId: 0n, - events: createEventHandlers(), + events: createEventHandlers({}), intents: [], }); -export function createEventHandlers(options?: Partial) { +export function createEventHandlers(events: Partial): EventHandlers { + function ignore() {} + return { - debug: () => undefined, - // PROVIDED OPTIONS OVERRIDE EVERYTHING ABOVE - ...options, + channelCreate: events.channelCreate ?? ignore, + debug: events.debug ?? ignore, + dispatchRequirements: events.dispatchRequirements ?? ignore, }; } @@ -114,6 +133,9 @@ export function createRestManager(options: CreateRestManagerOptions) { export async function startBot(bot: Bot) { const transformers = createTransformers(bot.transformers); + // SETUP UTILS + bot.utils = createUtils({}); + // SETUP CACHE bot.users = new Collection(); @@ -124,6 +146,28 @@ export async function startBot(bot: Bot) { bot.gateway = createGatewayManager({}); } +export function createUtils(options: Partial) { + return { + snowflakeToBigint, + bigintToSnowflake, + calculateShardId, + delay, + iconHashToBigInt, + iconBigintToHash, + loopObject, + }; +} + +export interface HelperUtils { + snowflakeToBigint: typeof snowflakeToBigint; + bigintToSnowflake: typeof bigintToSnowflake; + calculateShardId: typeof calculateShardId; + delay: typeof delay; + iconHashToBigInt: typeof iconHashToBigInt; + iconBigintToHash: typeof iconBigintToHash; + loopObject: typeof loopObject; +} + export function createGatewayManager(options: Partial): GatewayManager { return { secretKey: options.secretKey ?? "", @@ -181,7 +225,7 @@ export interface CreateBotOptions { token: string; botId: bigint; applicationId?: bigint; - events: Partial; + events: EventHandlers; intents: (keyof typeof DiscordGatewayIntents)[]; botGatewayData?: GetGatewayBot; rest?: Omit; @@ -192,6 +236,7 @@ export type UnPromise> = T extends Promise ? export type CreatedBot = UnPromise>; export type Bot = CreatedBot & { + utils: HelperUtils; rest: RestManager; gateway: GatewayManager; transformers: Transformers; @@ -326,4 +371,21 @@ export interface GatewayManager { export interface EventHandlers { debug: (text: string) => unknown; channelCreate: (bot: Bot, channel: DiscordenoChannel) => unknown; + dispatchRequirements: (bot: Bot, data: GatewayPayload, shardId: number) => Promise | unknown; +} + +export function createBotConstants() { + return { + DISCORDENO_VERSION, + USER_AGENT, + BASE_URL: baseEndpoints.BASE_URL, + CDN_URL: baseEndpoints.CDN_URL, + endpoints, + regexes: { + SLASH_COMMANDS_NAME_REGEX, + CONTEXT_MENU_COMMANDS_NAME_REGEX, + CHANNEL_MENTION_REGEX, + DISCORD_SNOWFLAKE_REGEX, + }, + }; } diff --git a/src/helpers/webhooks/create_webhook.ts b/src/helpers/webhooks/create_webhook.ts index 9853306b8..db413d602 100644 --- a/src/helpers/webhooks/create_webhook.ts +++ b/src/helpers/webhooks/create_webhook.ts @@ -1,30 +1,25 @@ -import { rest } from "../../rest/rest.ts"; -import { Errors } from "../../types/discordeno/errors.ts"; +import { Bot } from "../../bot.ts"; import type { CreateWebhook } from "../../types/webhooks/create_webhook.ts"; import type { Webhook } from "../../types/webhooks/webhook.ts"; -import { endpoints } from "../../util/constants.ts"; -import { requireBotChannelPermissions } from "../../util/permissions.ts"; -import { urlToBase64 } from "../../util/utils.ts"; -import { validateLength } from "../../util/validate_length.ts"; /** * Create a new webhook. Requires the MANAGE_WEBHOOKS permission. Returns a webhook object on success. Webhook names follow our naming restrictions that can be found in our Usernames and Nicknames documentation, with the following additional stipulations: * * Webhook names cannot be: 'clyde' */ -export async function createWebhook(channelId: bigint, options: CreateWebhook) { - await requireBotChannelPermissions(channelId, ["MANAGE_WEBHOOKS"]); +export async function createWebhook(bot: Bot, channelId: bigint, options: CreateWebhook) { + await bot.utils.requireBotChannelPermissions(channelId, ["MANAGE_WEBHOOKS"]); if ( // Specific usernames that discord does not allow options.name === "clyde" || - !validateLength(options.name, { min: 2, max: 32 }) + !bot.utils.validateLength(options.name, { min: 2, max: 32 }) ) { - throw new Error(Errors.INVALID_WEBHOOK_NAME); + throw new Error(bot.constants.Errors.INVALID_WEBHOOK_NAME); } - return await rest.runMethod("post", endpoints.CHANNEL_WEBHOOKS(channelId), { + return await bot.rest.runMethod(bot.rest, "post", bot.constants.endpoints.CHANNEL_WEBHOOKS(channelId), { ...options, - avatar: options.avatar ? await urlToBase64(options.avatar) : undefined, + avatar: options.avatar ? await bot.utils.urlToBase64(options.avatar) : undefined, }); } diff --git a/src/util/cache_members.ts b/src/util/cache_members.ts deleted file mode 100644 index f229ea92b..000000000 --- a/src/util/cache_members.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { eventHandlers } from "../bot.ts"; -import { cacheHandlers } from "../cache.ts"; -import { structures } from "../structures/mod.ts"; -import { GuildMemberWithUser } from "../types/members/guild_member.ts"; - -const guildMemberQueue = new Map void }>(); -let processingQueue = false; - -/** Cache all guild members without need to worry about overwriting something. */ -// deno-lint-ignore require-await -export async function cacheMembers(guildId: bigint, members: GuildMemberWithUser[]) { - if (!members.length) return; - - return new Promise((resolve) => { - guildMemberQueue.set(guildId, { members, resolve }); - startQueue(); - }); -} - -async function startQueue() { - if (processingQueue) return; - - processingQueue = true; - - while (guildMemberQueue.size) { - eventHandlers.debug?.("loop", "Running whille loop in cache_members file."); - const [guildId, queue]: [bigint, { members: GuildMemberWithUser[]; resolve: (value?: unknown) => void }] = - guildMemberQueue.entries().next().value; - - await Promise.allSettled([ - queue.members.map(async (member) => { - const discordenoMember = await structures.createDiscordenoMember(member, guildId); - - await cacheHandlers.set("members", discordenoMember.id, discordenoMember); - }), - ]); - - queue.resolve?.(); - - guildMemberQueue.delete(guildId); - } - - processingQueue = false; -} diff --git a/src/util/dispatch_requirements.ts b/src/util/dispatch_requirements.ts index a7405d424..b4167e091 100644 --- a/src/util/dispatch_requirements.ts +++ b/src/util/dispatch_requirements.ts @@ -1,13 +1,7 @@ import { Bot } 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"; +import { SnakeCasedPropertiesDeep } from "../types/util.ts"; const processing = new Set(); @@ -17,7 +11,7 @@ export async function dispatchRequirements(bot: Bot, data: DiscordGatewayPayload // 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( + const id = bot.utils.snowflakeToBigint( (data.t && ["GUILD_UPDATE"].includes(data.t) ? // deno-lint-ignore no-explicit-any (data.d as any)?.id @@ -25,11 +19,11 @@ export async function dispatchRequirements(bot: Bot, data: DiscordGatewayPayload (data.d as any)?.guild_id) ?? "" ); - if (!id || cache.activeGuildIds.has(id)) return; + if (!id || bot.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); + if (bot.guilds.has(id)) { + bot.activeGuildIds.add(id); return; } @@ -38,7 +32,7 @@ export async function dispatchRequirements(bot: Bot, data: DiscordGatewayPayload let runs = 0; do { - await delay(500); + await bot.utils.delay(500); runs++; } while (processing.has(id) && runs < 40); @@ -54,10 +48,12 @@ export async function dispatchRequirements(bot: Bot, data: DiscordGatewayPayload // New guild id has appeared, fetch all relevant data bot.events.debug(`[DISPATCH] New Guild ID has appeared: ${id} in ${data.t} event`); - const rawGuild = (await getGuild(id, { - counts: true, - addToCache: false, - }).catch(console.log)) as Guild | undefined; + const rawGuild = (await bot.helpers + .getGuild(id, { + counts: true, + addToCache: false, + }) + .catch(console.log)) as SnakeCasedPropertiesDeep | undefined; if (!rawGuild) { processing.delete(id); @@ -67,8 +63,8 @@ export async function dispatchRequirements(bot: Bot, data: DiscordGatewayPayload bot.events.debug(`[DISPATCH] Guild ID ${id} has been found. ${rawGuild.name}`); const [channels, botMember] = await Promise.all([ - getChannels(id, false), - getMember(id, bot.id, { force: true }), + bot.helpers.getChannels(id, false), + bot.helpers.getMember(id, bot.id, { force: true }), ]).catch((error) => { bot.events.debug(error); return []; @@ -81,17 +77,18 @@ export async function dispatchRequirements(bot: Bot, data: DiscordGatewayPayload ); } - const guild = await structures.createDiscordenoGuild( - { ...rawGuild, memberCount: rawGuild.approximateMemberCount }, - shardId - ); + const guild = await bot.transformers.guild(bot, { + ...rawGuild, + member_count: rawGuild.approximateMemberCount, + shardId, + }); // Add to cache - cache.guilds.set(id, guild); - cache.dispatchedGuildIds.delete(id); + bot.guilds.set(id, guild); + bot.dispatchedGuildIds.delete(id); channels.forEach((channel) => { - cache.dispatchedChannelIds.delete(channel.id); - cache.channels.set(channel.id, channel); + bot.dispatchedChannelIds.delete(channel.id); + bot.channels.set(channel.id, channel); }); processing.delete(id); diff --git a/src/util/loop_object.ts b/src/util/loop_object.ts index c3d45c004..ae404dd6f 100644 --- a/src/util/loop_object.ts +++ b/src/util/loop_object.ts @@ -1,6 +1,6 @@ -import { eventHandlers } from "../bot.ts"; +import { Bot } from "../bot.ts"; -export function loopObject(obj: {}, handler: (value: unknown, key: string) => unknown, log: string) { +export function loopObject(bot: Bot, obj: {}, handler: (value: unknown, key: string) => unknown, log: string) { let res: Record | unknown[] = {}; if (Array.isArray(obj)) { @@ -9,18 +9,21 @@ export function loopObject(obj: {}, handler: (value: unknown, key: strin for (const o of obj) { if (typeof o === "object" && !Array.isArray(o) && o !== null) { // A nested object - res.push(loopObject(o as {}, handler, log)); + res.push(loopObject(bot, o as {}, handler, log)); } else { res.push(handler(o, "array")); } } } else { for (const [key, value] of Object.entries(obj)) { - eventHandlers.debug?.("loop", log); + bot.events.debug(log); - if (typeof value === "object" && !Array.isArray(value) && value !== null && !(value instanceof Blob)) { + if ( + Array.isArray(value) || + (typeof value === "object" && !Array.isArray(value) && value !== null && !(value instanceof Blob)) + ) { // A nested object - res[key] = loopObject(value as {}, handler, log); + res[key] = loopObject(bot, value as {}, handler, log); } else { res[key] = handler(value, key); }