diff --git a/packages/core/README.md b/packages/core/README.md index 46540e2fd..b57f89ea6 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -35,7 +35,7 @@ pnpm add @discordjs/core ```ts import { REST } from '@discordjs/rest'; import { WebSocketManager } from '@discordjs/ws'; -import { GatewayIntentBits, InteractionType, MessageFlags, createClient } from '@discordjs/core'; +import { GatewayIntentBits, InteractionType, MessageFlags, Client } from '@discordjs/core'; // Create REST and WebSocket managers directly const rest = new REST({ version: '10' }).setToken(token); @@ -46,11 +46,11 @@ const ws = new WebSocketManager({ }); // Create a client to emit relevant events. -const client = createClient({ rest, ws }); +const client = new Client({ rest, ws }); // Listen for interactions // Each event contains an `api` prop along with the event data that allows you to interface with the Discord REST API -client.on('interactionCreate', async ({ interaction, api }) => { +client.on(GatewayDispatchEvents.InteractionCreate, async ({ data: interaction, api }) => { if (!(interaction.type === InteractionType.ApplicationCommand) || interaction.data.name !== 'ping') { return; } @@ -59,7 +59,7 @@ client.on('interactionCreate', async ({ interaction, api }) => { }); // Listen for the ready event -client.on('ready', () => console.log('Ready!')); +client.once(GatewayDispatchEvents.Ready, () => console.log('Ready!')); // Start the WebSocket connection. ws.connect(); diff --git a/packages/core/package.json b/packages/core/package.json index 31477edd0..2825cfb71 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -46,7 +46,9 @@ "homepage": "https://discord.js.org", "dependencies": { "@discordjs/rest": "workspace:^", + "@discordjs/util": "workspace:^", "@discordjs/ws": "workspace:^", + "@sapphire/snowflake": "^3.3.0", "@vladfrangu/async_event_emitter": "^2.1.2", "discord-api-types": "^0.37.23" }, diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 04652a730..2b69c75f2 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1,66 +1,74 @@ +import { setTimeout } from 'node:timers'; import type { REST } from '@discordjs/rest'; +import { calculateShardId } from '@discordjs/util'; import { WebSocketShardEvents, type WebSocketManager } from '@discordjs/ws'; +import { DiscordSnowflake } from '@sapphire/snowflake'; import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; -import type { - GatewayAutoModerationActionExecutionDispatchData, - GatewayAutoModerationRuleCreateDispatchData, - GatewayAutoModerationRuleDeleteDispatchData, - GatewayAutoModerationRuleUpdateDispatchData, - GatewayChannelCreateDispatchData, - GatewayChannelDeleteDispatchData, - GatewayChannelPinsUpdateDispatchData, - GatewayChannelUpdateDispatchData, +import { GatewayDispatchEvents, - GatewayGuildBanAddDispatchData, - GatewayGuildBanRemoveDispatchData, - GatewayGuildCreateDispatchData, - GatewayGuildDeleteDispatchData, - GatewayGuildEmojisUpdateDispatchData, - GatewayGuildIntegrationsUpdateDispatchData, - GatewayGuildMemberAddDispatchData, - GatewayGuildMemberRemoveDispatchData, - GatewayGuildMembersChunkDispatchData, - GatewayGuildMemberUpdateDispatchData, - GatewayGuildRoleCreateDispatchData, - GatewayGuildRoleDeleteDispatchData, - GatewayGuildRoleUpdateDispatchData, - GatewayGuildScheduledEventCreateDispatchData, - GatewayGuildScheduledEventDeleteDispatchData, - GatewayGuildScheduledEventUpdateDispatchData, - GatewayGuildScheduledEventUserAddDispatchData, - GatewayGuildScheduledEventUserRemoveDispatchData, - GatewayGuildStickersUpdateDispatchData, - GatewayGuildUpdateDispatchData, - GatewayIntegrationCreateDispatchData, - GatewayIntegrationDeleteDispatchData, - GatewayIntegrationUpdateDispatchData, - GatewayInteractionCreateDispatchData, - GatewayInviteCreateDispatchData, - GatewayInviteDeleteDispatchData, - GatewayMessageCreateDispatchData, - GatewayMessageDeleteBulkDispatchData, - GatewayMessageDeleteDispatchData, - GatewayMessageReactionAddDispatchData, - GatewayMessageReactionRemoveAllDispatchData, - GatewayMessageReactionRemoveDispatchData, - GatewayMessageReactionRemoveEmojiDispatchData, - GatewayMessageUpdateDispatchData, - GatewayPresenceUpdateDispatchData, - GatewayReadyDispatchData, - GatewayStageInstanceCreateDispatchData, - GatewayStageInstanceDeleteDispatchData, - GatewayStageInstanceUpdateDispatchData, - GatewayThreadCreateDispatchData, - GatewayThreadDeleteDispatchData, - GatewayThreadListSyncDispatchData, - GatewayThreadMembersUpdateDispatchData, - GatewayThreadMemberUpdateDispatchData, - GatewayThreadUpdateDispatchData, - GatewayTypingStartDispatchData, - GatewayUserUpdateDispatchData, - GatewayVoiceServerUpdateDispatchData, - GatewayVoiceStateUpdateDispatchData, - GatewayWebhooksUpdateDispatchData, + GatewayOpcodes, + type GatewayVoiceStateUpdateData, + type APIGuildMember, + type GatewayAutoModerationActionExecutionDispatchData, + type GatewayAutoModerationRuleCreateDispatchData, + type GatewayAutoModerationRuleDeleteDispatchData, + type GatewayAutoModerationRuleUpdateDispatchData, + type GatewayChannelCreateDispatchData, + type GatewayChannelDeleteDispatchData, + type GatewayChannelPinsUpdateDispatchData, + type GatewayChannelUpdateDispatchData, + type GatewayGuildBanAddDispatchData, + type GatewayGuildBanRemoveDispatchData, + type GatewayGuildCreateDispatchData, + type GatewayGuildDeleteDispatchData, + type GatewayGuildEmojisUpdateDispatchData, + type GatewayGuildIntegrationsUpdateDispatchData, + type GatewayGuildMemberAddDispatchData, + type GatewayGuildMemberRemoveDispatchData, + type GatewayGuildMembersChunkDispatchData, + type GatewayGuildMemberUpdateDispatchData, + type GatewayGuildRoleCreateDispatchData, + type GatewayGuildRoleDeleteDispatchData, + type GatewayGuildRoleUpdateDispatchData, + type GatewayGuildScheduledEventCreateDispatchData, + type GatewayGuildScheduledEventDeleteDispatchData, + type GatewayGuildScheduledEventUpdateDispatchData, + type GatewayGuildScheduledEventUserAddDispatchData, + type GatewayGuildScheduledEventUserRemoveDispatchData, + type GatewayGuildStickersUpdateDispatchData, + type GatewayGuildUpdateDispatchData, + type GatewayIntegrationCreateDispatchData, + type GatewayIntegrationDeleteDispatchData, + type GatewayIntegrationUpdateDispatchData, + type GatewayInteractionCreateDispatchData, + type GatewayInviteCreateDispatchData, + type GatewayInviteDeleteDispatchData, + type GatewayMessageCreateDispatchData, + type GatewayMessageDeleteBulkDispatchData, + type GatewayMessageDeleteDispatchData, + type GatewayMessageReactionAddDispatchData, + type GatewayMessageReactionRemoveAllDispatchData, + type GatewayMessageReactionRemoveDispatchData, + type GatewayMessageReactionRemoveEmojiDispatchData, + type GatewayMessageUpdateDispatchData, + type GatewayPresenceUpdateDispatchData, + type GatewayReadyDispatchData, + type GatewayRequestGuildMembersData, + type GatewayStageInstanceCreateDispatchData, + type GatewayStageInstanceDeleteDispatchData, + type GatewayStageInstanceUpdateDispatchData, + type GatewayThreadCreateDispatchData, + type GatewayThreadDeleteDispatchData, + type GatewayThreadListSyncDispatchData, + type GatewayThreadMembersUpdateDispatchData, + type GatewayThreadMemberUpdateDispatchData, + type GatewayThreadUpdateDispatchData, + type GatewayTypingStartDispatchData, + type GatewayUserUpdateDispatchData, + type GatewayVoiceServerUpdateDispatchData, + type GatewayVoiceStateUpdateDispatchData, + type GatewayWebhooksUpdateDispatchData, + type GatewayPresenceUpdateData, } from 'discord-api-types/v10'; import { API } from './api/index.js'; @@ -158,22 +166,106 @@ export interface ClientOptions { ws: WebSocketManager; } -export function createClient({ rest, ws }: ClientOptions) { - const api = new API(rest); - const emitter = new AsyncEventEmitter(); +export class Client extends AsyncEventEmitter { + public readonly rest: REST; - function wrapIntrinsicProps(obj: T, shardId: number): WithIntrinsicProps { + public readonly ws: WebSocketManager; + + public readonly api: API; + + public constructor({ rest, ws }: ClientOptions) { + super(); + this.rest = rest; + this.ws = ws; + this.api = new API(rest); + + this.ws.on(WebSocketShardEvents.Dispatch, ({ data: dispatch, shardId }) => { + // @ts-expect-error event props can't be resolved properly, but they are correct + this.emit(dispatch.t, this.wrapIntrinsicProps(dispatch.d, shardId)); + }); + } + + /** + * Requests guild members from the gateway. + * + * @see {@link https://discord.com/developers/docs/topics/gateway-events#request-guild-members} + * @param options - The options for the request + * @param timeout - The timeout for waiting for each guild members chunk event + */ + public async requestGuildMembers(options: GatewayRequestGuildMembersData, timeout = 10_000) { + const shardId = calculateShardId(options.guild_id, await this.ws.getShardCount()); + const nonce = options.nonce ?? DiscordSnowflake.generate().toString(); + + const promise = new Promise((resolve, reject) => { + const guildMembers: APIGuildMember[] = []; + + const timer = setTimeout(() => { + reject(new Error('Request timed out')); + }, timeout); + + const handler = ({ data }: MappedEvents[GatewayDispatchEvents.GuildMembersChunk][0]) => { + timer.refresh(); + + if (data.nonce !== nonce) return; + + guildMembers.push(...data.members); + + if (data.chunk_index >= data.chunk_count - 1) { + this.off(GatewayDispatchEvents.GuildMembersChunk, handler); + resolve(guildMembers); + } + }; + + this.on(GatewayDispatchEvents.GuildMembersChunk, handler); + }); + + await this.ws.send(shardId, { + op: GatewayOpcodes.RequestGuildMembers, + // eslint-disable-next-line id-length + d: { + ...options, + nonce, + }, + }); + + return promise; + } + + /** + * Updates the voice state of the bot user + * + * @see {@link https://discord.com/developers/docs/topics/gateway-events#update-voice-state} + * @param options - The options for updating the voice state + */ + public async updateVoiceState(options: GatewayVoiceStateUpdateData) { + const shardId = calculateShardId(options.guild_id, await this.ws.getShardCount()); + + await this.ws.send(shardId, { + op: GatewayOpcodes.VoiceStateUpdate, + // eslint-disable-next-line id-length + d: options, + }); + } + + /** + * Updates the presence of the bot user + * + * @param shardId - The id of the shard to update the presence in + * @param options - The options for updating the presence + */ + public async updatePresence(shardId: number, options: GatewayPresenceUpdateData) { + await this.ws.send(shardId, { + op: GatewayOpcodes.PresenceUpdate, + // eslint-disable-next-line id-length + d: options, + }); + } + + private wrapIntrinsicProps(obj: T, shardId: number): WithIntrinsicProps { return { - api, + api: this.api, shardId, data: obj, }; } - - ws.on(WebSocketShardEvents.Dispatch, ({ data: dispatch, shardId }) => { - // @ts-expect-error event props can't be resolved properly, but they are correct - emitter.emit(dispatch.t, wrapIntrinsicProps(dispatch.d, shardId)); - }); - - return emitter; } diff --git a/packages/util/src/functions/calculateShardId.ts b/packages/util/src/functions/calculateShardId.ts new file mode 100644 index 000000000..8beae43e2 --- /dev/null +++ b/packages/util/src/functions/calculateShardId.ts @@ -0,0 +1,3 @@ +export function calculateShardId(guildId: string, shardCount: number) { + return Number((BigInt(guildId) >> 22n) % BigInt(shardCount)); +} diff --git a/packages/util/src/functions/index.ts b/packages/util/src/functions/index.ts index 93173c1d2..f5fdc6654 100644 --- a/packages/util/src/functions/index.ts +++ b/packages/util/src/functions/index.ts @@ -1,2 +1,3 @@ export * from './lazy.js'; export * from './range.js'; +export * from './calculateShardId.js'; diff --git a/yarn.lock b/yarn.lock index 84c41490d..28b7be507 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2095,9 +2095,11 @@ __metadata: resolution: "@discordjs/core@workspace:packages/core" dependencies: "@discordjs/rest": "workspace:^" + "@discordjs/util": "workspace:^" "@discordjs/ws": "workspace:^" "@favware/cliff-jumper": ^1.9.0 "@microsoft/api-extractor": ^7.33.6 + "@sapphire/snowflake": ^3.3.0 "@types/node": 16.18.4 "@vitest/coverage-c8": ^0.25.3 "@vladfrangu/async_event_emitter": ^2.1.2 @@ -3898,6 +3900,13 @@ __metadata: languageName: node linkType: hard +"@sapphire/snowflake@npm:^3.3.0": + version: 3.3.0 + resolution: "@sapphire/snowflake@npm:3.3.0" + checksum: 122bbe325d596d670650c5c037d7f80a85a280ef5d5170dcb11030252773defa0df76277bcd28e663abe9c206310dcc596e3be32666fc6c53dede2798c3109da + languageName: node + linkType: hard + "@sapphire/utilities@npm:3.11.0, @sapphire/utilities@npm:^3.11.0": version: 3.11.0 resolution: "@sapphire/utilities@npm:3.11.0"