mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
feat(core): implement some ws send events (#8941)
* feat(core): implement some ws send events * fix: check guild id and add a timeout * fix: only check for nonce * chore: update readme * chore: make requested changes * chore: make methods consistent * chore: fix readme example * chore: move shard ID calculation to util Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
@@ -35,7 +35,7 @@ pnpm add @discordjs/core
|
|||||||
```ts
|
```ts
|
||||||
import { REST } from '@discordjs/rest';
|
import { REST } from '@discordjs/rest';
|
||||||
import { WebSocketManager } from '@discordjs/ws';
|
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
|
// Create REST and WebSocket managers directly
|
||||||
const rest = new REST({ version: '10' }).setToken(token);
|
const rest = new REST({ version: '10' }).setToken(token);
|
||||||
@@ -46,11 +46,11 @@ const ws = new WebSocketManager({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create a client to emit relevant events.
|
// Create a client to emit relevant events.
|
||||||
const client = createClient({ rest, ws });
|
const client = new Client({ rest, ws });
|
||||||
|
|
||||||
// Listen for interactions
|
// Listen for interactions
|
||||||
// Each event contains an `api` prop along with the event data that allows you to interface with the Discord REST API
|
// 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') {
|
if (!(interaction.type === InteractionType.ApplicationCommand) || interaction.data.name !== 'ping') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ client.on('interactionCreate', async ({ interaction, api }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Listen for the ready event
|
// Listen for the ready event
|
||||||
client.on('ready', () => console.log('Ready!'));
|
client.once(GatewayDispatchEvents.Ready, () => console.log('Ready!'));
|
||||||
|
|
||||||
// Start the WebSocket connection.
|
// Start the WebSocket connection.
|
||||||
ws.connect();
|
ws.connect();
|
||||||
|
|||||||
@@ -46,7 +46,9 @@
|
|||||||
"homepage": "https://discord.js.org",
|
"homepage": "https://discord.js.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/rest": "workspace:^",
|
"@discordjs/rest": "workspace:^",
|
||||||
|
"@discordjs/util": "workspace:^",
|
||||||
"@discordjs/ws": "workspace:^",
|
"@discordjs/ws": "workspace:^",
|
||||||
|
"@sapphire/snowflake": "^3.3.0",
|
||||||
"@vladfrangu/async_event_emitter": "^2.1.2",
|
"@vladfrangu/async_event_emitter": "^2.1.2",
|
||||||
"discord-api-types": "^0.37.23"
|
"discord-api-types": "^0.37.23"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,66 +1,74 @@
|
|||||||
|
import { setTimeout } from 'node:timers';
|
||||||
import type { REST } from '@discordjs/rest';
|
import type { REST } from '@discordjs/rest';
|
||||||
|
import { calculateShardId } from '@discordjs/util';
|
||||||
import { WebSocketShardEvents, type WebSocketManager } from '@discordjs/ws';
|
import { WebSocketShardEvents, type WebSocketManager } from '@discordjs/ws';
|
||||||
|
import { DiscordSnowflake } from '@sapphire/snowflake';
|
||||||
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
|
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
|
||||||
import type {
|
import {
|
||||||
GatewayAutoModerationActionExecutionDispatchData,
|
|
||||||
GatewayAutoModerationRuleCreateDispatchData,
|
|
||||||
GatewayAutoModerationRuleDeleteDispatchData,
|
|
||||||
GatewayAutoModerationRuleUpdateDispatchData,
|
|
||||||
GatewayChannelCreateDispatchData,
|
|
||||||
GatewayChannelDeleteDispatchData,
|
|
||||||
GatewayChannelPinsUpdateDispatchData,
|
|
||||||
GatewayChannelUpdateDispatchData,
|
|
||||||
GatewayDispatchEvents,
|
GatewayDispatchEvents,
|
||||||
GatewayGuildBanAddDispatchData,
|
GatewayOpcodes,
|
||||||
GatewayGuildBanRemoveDispatchData,
|
type GatewayVoiceStateUpdateData,
|
||||||
GatewayGuildCreateDispatchData,
|
type APIGuildMember,
|
||||||
GatewayGuildDeleteDispatchData,
|
type GatewayAutoModerationActionExecutionDispatchData,
|
||||||
GatewayGuildEmojisUpdateDispatchData,
|
type GatewayAutoModerationRuleCreateDispatchData,
|
||||||
GatewayGuildIntegrationsUpdateDispatchData,
|
type GatewayAutoModerationRuleDeleteDispatchData,
|
||||||
GatewayGuildMemberAddDispatchData,
|
type GatewayAutoModerationRuleUpdateDispatchData,
|
||||||
GatewayGuildMemberRemoveDispatchData,
|
type GatewayChannelCreateDispatchData,
|
||||||
GatewayGuildMembersChunkDispatchData,
|
type GatewayChannelDeleteDispatchData,
|
||||||
GatewayGuildMemberUpdateDispatchData,
|
type GatewayChannelPinsUpdateDispatchData,
|
||||||
GatewayGuildRoleCreateDispatchData,
|
type GatewayChannelUpdateDispatchData,
|
||||||
GatewayGuildRoleDeleteDispatchData,
|
type GatewayGuildBanAddDispatchData,
|
||||||
GatewayGuildRoleUpdateDispatchData,
|
type GatewayGuildBanRemoveDispatchData,
|
||||||
GatewayGuildScheduledEventCreateDispatchData,
|
type GatewayGuildCreateDispatchData,
|
||||||
GatewayGuildScheduledEventDeleteDispatchData,
|
type GatewayGuildDeleteDispatchData,
|
||||||
GatewayGuildScheduledEventUpdateDispatchData,
|
type GatewayGuildEmojisUpdateDispatchData,
|
||||||
GatewayGuildScheduledEventUserAddDispatchData,
|
type GatewayGuildIntegrationsUpdateDispatchData,
|
||||||
GatewayGuildScheduledEventUserRemoveDispatchData,
|
type GatewayGuildMemberAddDispatchData,
|
||||||
GatewayGuildStickersUpdateDispatchData,
|
type GatewayGuildMemberRemoveDispatchData,
|
||||||
GatewayGuildUpdateDispatchData,
|
type GatewayGuildMembersChunkDispatchData,
|
||||||
GatewayIntegrationCreateDispatchData,
|
type GatewayGuildMemberUpdateDispatchData,
|
||||||
GatewayIntegrationDeleteDispatchData,
|
type GatewayGuildRoleCreateDispatchData,
|
||||||
GatewayIntegrationUpdateDispatchData,
|
type GatewayGuildRoleDeleteDispatchData,
|
||||||
GatewayInteractionCreateDispatchData,
|
type GatewayGuildRoleUpdateDispatchData,
|
||||||
GatewayInviteCreateDispatchData,
|
type GatewayGuildScheduledEventCreateDispatchData,
|
||||||
GatewayInviteDeleteDispatchData,
|
type GatewayGuildScheduledEventDeleteDispatchData,
|
||||||
GatewayMessageCreateDispatchData,
|
type GatewayGuildScheduledEventUpdateDispatchData,
|
||||||
GatewayMessageDeleteBulkDispatchData,
|
type GatewayGuildScheduledEventUserAddDispatchData,
|
||||||
GatewayMessageDeleteDispatchData,
|
type GatewayGuildScheduledEventUserRemoveDispatchData,
|
||||||
GatewayMessageReactionAddDispatchData,
|
type GatewayGuildStickersUpdateDispatchData,
|
||||||
GatewayMessageReactionRemoveAllDispatchData,
|
type GatewayGuildUpdateDispatchData,
|
||||||
GatewayMessageReactionRemoveDispatchData,
|
type GatewayIntegrationCreateDispatchData,
|
||||||
GatewayMessageReactionRemoveEmojiDispatchData,
|
type GatewayIntegrationDeleteDispatchData,
|
||||||
GatewayMessageUpdateDispatchData,
|
type GatewayIntegrationUpdateDispatchData,
|
||||||
GatewayPresenceUpdateDispatchData,
|
type GatewayInteractionCreateDispatchData,
|
||||||
GatewayReadyDispatchData,
|
type GatewayInviteCreateDispatchData,
|
||||||
GatewayStageInstanceCreateDispatchData,
|
type GatewayInviteDeleteDispatchData,
|
||||||
GatewayStageInstanceDeleteDispatchData,
|
type GatewayMessageCreateDispatchData,
|
||||||
GatewayStageInstanceUpdateDispatchData,
|
type GatewayMessageDeleteBulkDispatchData,
|
||||||
GatewayThreadCreateDispatchData,
|
type GatewayMessageDeleteDispatchData,
|
||||||
GatewayThreadDeleteDispatchData,
|
type GatewayMessageReactionAddDispatchData,
|
||||||
GatewayThreadListSyncDispatchData,
|
type GatewayMessageReactionRemoveAllDispatchData,
|
||||||
GatewayThreadMembersUpdateDispatchData,
|
type GatewayMessageReactionRemoveDispatchData,
|
||||||
GatewayThreadMemberUpdateDispatchData,
|
type GatewayMessageReactionRemoveEmojiDispatchData,
|
||||||
GatewayThreadUpdateDispatchData,
|
type GatewayMessageUpdateDispatchData,
|
||||||
GatewayTypingStartDispatchData,
|
type GatewayPresenceUpdateDispatchData,
|
||||||
GatewayUserUpdateDispatchData,
|
type GatewayReadyDispatchData,
|
||||||
GatewayVoiceServerUpdateDispatchData,
|
type GatewayRequestGuildMembersData,
|
||||||
GatewayVoiceStateUpdateDispatchData,
|
type GatewayStageInstanceCreateDispatchData,
|
||||||
GatewayWebhooksUpdateDispatchData,
|
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';
|
} from 'discord-api-types/v10';
|
||||||
import { API } from './api/index.js';
|
import { API } from './api/index.js';
|
||||||
|
|
||||||
@@ -158,22 +166,106 @@ export interface ClientOptions {
|
|||||||
ws: WebSocketManager;
|
ws: WebSocketManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createClient({ rest, ws }: ClientOptions) {
|
export class Client extends AsyncEventEmitter<ManagerShardEventsMap> {
|
||||||
const api = new API(rest);
|
public readonly rest: REST;
|
||||||
const emitter = new AsyncEventEmitter<ManagerShardEventsMap>();
|
|
||||||
|
|
||||||
function wrapIntrinsicProps<T>(obj: T, shardId: number): WithIntrinsicProps<T> {
|
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<APIGuildMember[]>((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<T>(obj: T, shardId: number): WithIntrinsicProps<T> {
|
||||||
return {
|
return {
|
||||||
api,
|
api: this.api,
|
||||||
shardId,
|
shardId,
|
||||||
data: obj,
|
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
packages/util/src/functions/calculateShardId.ts
Normal file
3
packages/util/src/functions/calculateShardId.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function calculateShardId(guildId: string, shardCount: number) {
|
||||||
|
return Number((BigInt(guildId) >> 22n) % BigInt(shardCount));
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './lazy.js';
|
export * from './lazy.js';
|
||||||
export * from './range.js';
|
export * from './range.js';
|
||||||
|
export * from './calculateShardId.js';
|
||||||
|
|||||||
@@ -2095,9 +2095,11 @@ __metadata:
|
|||||||
resolution: "@discordjs/core@workspace:packages/core"
|
resolution: "@discordjs/core@workspace:packages/core"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@discordjs/rest": "workspace:^"
|
"@discordjs/rest": "workspace:^"
|
||||||
|
"@discordjs/util": "workspace:^"
|
||||||
"@discordjs/ws": "workspace:^"
|
"@discordjs/ws": "workspace:^"
|
||||||
"@favware/cliff-jumper": ^1.9.0
|
"@favware/cliff-jumper": ^1.9.0
|
||||||
"@microsoft/api-extractor": ^7.33.6
|
"@microsoft/api-extractor": ^7.33.6
|
||||||
|
"@sapphire/snowflake": ^3.3.0
|
||||||
"@types/node": 16.18.4
|
"@types/node": 16.18.4
|
||||||
"@vitest/coverage-c8": ^0.25.3
|
"@vitest/coverage-c8": ^0.25.3
|
||||||
"@vladfrangu/async_event_emitter": ^2.1.2
|
"@vladfrangu/async_event_emitter": ^2.1.2
|
||||||
@@ -3898,6 +3900,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@sapphire/utilities@npm:3.11.0, @sapphire/utilities@npm:^3.11.0":
|
||||||
version: 3.11.0
|
version: 3.11.0
|
||||||
resolution: "@sapphire/utilities@npm:3.11.0"
|
resolution: "@sapphire/utilities@npm:3.11.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user