Skip to content

Commit

Permalink
feat(core): implement some ws send events (#8941)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
suneettipirneni and kodiakhq[bot] committed Jan 5, 2023
1 parent 3407e1e commit 816aed4
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 75 deletions.
8 changes: 4 additions & 4 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand All @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
234 changes: 163 additions & 71 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -158,22 +166,106 @@ export interface ClientOptions {
ws: WebSocketManager;
}

export function createClient({ rest, ws }: ClientOptions) {
const api = new API(rest);
const emitter = new AsyncEventEmitter<ManagerShardEventsMap>();
export class Client extends AsyncEventEmitter<ManagerShardEventsMap> {
public readonly rest: REST;

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 {
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;
}
3 changes: 3 additions & 0 deletions packages/util/src/functions/calculateShardId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function calculateShardId(guildId: string, shardCount: number) {
return Number((BigInt(guildId) >> 22n) % BigInt(shardCount));
}
1 change: 1 addition & 0 deletions packages/util/src/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './lazy.js';
export * from './range.js';
export * from './calculateShardId.js';
9 changes: 9 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 816aed4

Please sign in to comment.