-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
client.ts
156 lines (131 loc) · 4.32 KB
/
client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { setTimeout } from 'node:timers';
import type { REST } from '@discordjs/rest';
import { calculateShardId } from '@discordjs/util';
import { WebSocketShardEvents } from '@discordjs/ws';
import { DiscordSnowflake } from '@sapphire/snowflake';
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
import { GatewayDispatchEvents, GatewayOpcodes } from 'discord-api-types/v10';
import type {
GatewayDispatchPayload,
APIGuildMember,
GatewayRequestGuildMembersData,
GatewayPresenceUpdateData,
GatewayVoiceStateUpdateData,
} from 'discord-api-types/v10';
import { API } from './api/index.js';
import type { Gateway } from './gateway/Gateway.js';
export interface IntrinsicProps {
/**
* The REST API
*/
api: API;
/**
* The id of the shard that emitted the event
*/
shardId: number;
}
export interface WithIntrinsicProps<T> extends IntrinsicProps {
data: T;
}
// need this to be its own type for some reason, the compiler doesn't behave the same way if we in-line it
type _DiscordEvents = {
[K in GatewayDispatchEvents]: GatewayDispatchPayload & {
t: K;
};
};
export type DiscordEvents = {
// @ts-expect-error - unclear why this is an error, this behaves as expected
[K in keyof _DiscordEvents]: _DiscordEvents[K]['d'];
};
export type MappedEvents = {
[K in keyof DiscordEvents]: [WithIntrinsicProps<DiscordEvents[K]>];
};
export interface ClientOptions {
gateway: Gateway;
rest: REST;
}
export class Client extends AsyncEventEmitter<MappedEvents> {
public readonly rest: REST;
public readonly gateway: Gateway;
public readonly api: API;
public constructor({ rest, gateway }: ClientOptions) {
super();
this.rest = rest;
this.gateway = gateway;
this.api = new API(rest);
this.gateway.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.gateway.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.gateway.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.gateway.getShardCount());
await this.gateway.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.gateway.send(shardId, {
op: GatewayOpcodes.PresenceUpdate,
// eslint-disable-next-line id-length
d: options,
});
}
private wrapIntrinsicProps<T>(obj: T, shardId: number): WithIntrinsicProps<T> {
return {
api: this.api,
shardId,
data: obj,
};
}
}