@wolfstar/plugin-gateway 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +505 -0
- package/dist/esm/index.d.ts +7438 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +11031 -0
- package/dist/esm/index.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://cdn.wolfstar.rocks/wolfstar-assets/wolfstar.png" alt="WolfStar" width="100" />
|
|
4
|
+
|
|
5
|
+
# @wolfstar/plugin-gateway
|
|
6
|
+
|
|
7
|
+
**Gateway events, Discord structures, and API managers for `@wolfstar/http-framework`.**
|
|
8
|
+
|
|
9
|
+
[](https://npmx.dev/package/@wolfstar/plugin-gateway)
|
|
10
|
+
[](https://npmx.dev/package/@wolfstar/plugin-gateway)
|
|
11
|
+
[](https://github.com/wolfstar-project/plugins/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
## Description
|
|
16
|
+
|
|
17
|
+
`@wolfstar/plugin-gateway` extends the
|
|
18
|
+
[`@wolfstar/http-framework`](https://www.npmjs.com/package/@wolfstar/http-framework) client with
|
|
19
|
+
Discord gateway events and API access. `GatewayClient.start()` loads the framework's pieces, starts
|
|
20
|
+
its HTTP interaction endpoint, and connects the gateway shards in one call. Commands and HTTP
|
|
21
|
+
interactions continue to use the framework's existing client.
|
|
22
|
+
|
|
23
|
+
The gateway connection uses [`@discordjs/ws`](https://www.npmjs.com/package/@discordjs/ws) for
|
|
24
|
+
sharding, reconnects, and session resumes. Actions turn dispatches into events containing structures
|
|
25
|
+
such as `Message`, `User`, and `Guild`; `EventGatewayListener` lets pieces in the `listeners`
|
|
26
|
+
directory handle those events. Managers such as `client.users` and `client.guilds` read from an
|
|
27
|
+
optional [`@wolfstar/plugin-cache`](../plugin-cache) cache and fetch missing data through
|
|
28
|
+
`@discordjs/core`. The same core API is available as `client.core.api`.
|
|
29
|
+
|
|
30
|
+
> [!NOTE]
|
|
31
|
+
> A gateway connection is long-lived: a `GatewayClient` needs a persistent process, unlike a bot
|
|
32
|
+
> only serving HTTP interactions.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add @wolfstar/plugin-gateway @wolfstar/plugin-cache
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createInMemoryCache } from "@wolfstar/plugin-cache";
|
|
44
|
+
import { GatewayClient } from "@wolfstar/plugin-gateway";
|
|
45
|
+
import { GatewayIntentBits } from "discord-api-types/v10";
|
|
46
|
+
|
|
47
|
+
const client = new GatewayClient({
|
|
48
|
+
intents:
|
|
49
|
+
GatewayIntentBits.Guilds | GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent,
|
|
50
|
+
cache: createInMemoryCache(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
client.on("messageCreate", async (message) => {
|
|
54
|
+
const guild = message.guildId ? await client.guilds.get(message.guildId) : undefined;
|
|
55
|
+
console.log(`${message.author.username} in ${guild?.name ?? "a DM"}: ${message.content}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await client.start({ listen: { port: 8080 } }); // loads pieces, starts HTTP, connects the gateway
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Options
|
|
62
|
+
|
|
63
|
+
On top of the `Client` options:
|
|
64
|
+
|
|
65
|
+
| Option | Default | Description |
|
|
66
|
+
| ----------------- | ----------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `intents` | — | The gateway intents. |
|
|
68
|
+
| `cache` | `undefined` | A `Cache` from `@wolfstar/plugin-cache`, see [Caching](#caching). |
|
|
69
|
+
| `shardCount` | `null` | Total shards across every process, `null` for Discord's recommendation. |
|
|
70
|
+
| `shardIds` | `null` | The shards this client runs, as an array or a `{ start, end }` range. `null` for all. |
|
|
71
|
+
| `gateway` | `{}` | Extra `@discordjs/ws` `WebSocketManager` options (`compression`, `initialPresence`, ...). |
|
|
72
|
+
| `cacheFailure` | `"skip"` | On a cache read/write failure, `"skip"` drops the event, `"emitUncached"` emits it from the payload. |
|
|
73
|
+
| `dispatchTimeout` | `30_000` | Milliseconds after which a dispatch still processing is reported as a `DispatchTimeoutError`. `null` disables it. |
|
|
74
|
+
|
|
75
|
+
`client.gateway` exposes the underlying `WebSocketManager`, e.g. to send presence updates.
|
|
76
|
+
|
|
77
|
+
## Events
|
|
78
|
+
|
|
79
|
+
| Event | Arguments |
|
|
80
|
+
| --------------------------------------------------------- | ---------------------------------------------- |
|
|
81
|
+
| `raw` | `payload`, `shardId` — every dispatch |
|
|
82
|
+
| `shardReady` | `shardId`, `user` |
|
|
83
|
+
| `shardResume` / `shardClose` / `shardError` | `shardId` / `shardId, code` / `error, shardId` |
|
|
84
|
+
| `guildCreate` | `guild` |
|
|
85
|
+
| `guildUpdate` | `oldGuild \| null`, `newGuild` |
|
|
86
|
+
| `guildDelete` | `guild \| null`, `data` |
|
|
87
|
+
| `channelCreate` / `channelDelete` | `channel` |
|
|
88
|
+
| `channelUpdate` | `oldChannel \| null`, `newChannel` |
|
|
89
|
+
| `threadCreate` / `threadUpdate` / `threadDelete` | same shapes as channels |
|
|
90
|
+
| `threadListSync` | `threads`, `members`, `data` |
|
|
91
|
+
| `threadMemberUpdate` | `oldMember \| null`, `newMember` |
|
|
92
|
+
| `threadMembersUpdate` | `added`, `removed`, `thread \| null`, `data` |
|
|
93
|
+
| `messageCreate` | `message` |
|
|
94
|
+
| `messageUpdate` | `oldMessage \| null`, `newMessage` |
|
|
95
|
+
| `messageDelete` | `message \| null`, `data` |
|
|
96
|
+
| `messageDeleteBulk` | `messages`, `data` |
|
|
97
|
+
| `messageReactionAdd` / `messageReactionRemove` | `reaction`, `user \| null`, `details` |
|
|
98
|
+
| `messageReactionRemoveAll` | `message \| null`, `reactions`, `data` |
|
|
99
|
+
| `messageReactionRemoveEmoji` | `reaction` |
|
|
100
|
+
| `messagePollVoteAdd` / `messagePollVoteRemove` | `answer`, `userId` |
|
|
101
|
+
| `guildMemberAdd` | `member` |
|
|
102
|
+
| `guildMemberUpdate` | `oldMember \| null`, `newMember` |
|
|
103
|
+
| `guildMemberRemove` | `member \| null`, `data` |
|
|
104
|
+
| `guildRoleCreate` / `guildRoleUpdate` / `guildRoleDelete` | same shapes as members |
|
|
105
|
+
| `userUpdate` | `oldUser \| null`, `newUser` |
|
|
106
|
+
| `emojiCreate` / `emojiDelete` | `emoji` |
|
|
107
|
+
| `emojiUpdate` | `oldEmoji`, `newEmoji` |
|
|
108
|
+
| `stickerCreate` / `stickerDelete` | `sticker` |
|
|
109
|
+
| `stickerUpdate` | `oldSticker`, `newSticker` |
|
|
110
|
+
| `inviteCreate` | `invite` |
|
|
111
|
+
| `inviteDelete` | `invite \| null`, `data` |
|
|
112
|
+
| `voiceStateUpdate` | `oldState \| null`, `newState` |
|
|
113
|
+
| `presenceUpdate` | `oldPresence \| null`, `newPresence` |
|
|
114
|
+
| `guildScheduledEventCreate` / `guildScheduledEventDelete` | `event` |
|
|
115
|
+
| `guildScheduledEventUpdate` | `oldEvent \| null`, `newEvent` |
|
|
116
|
+
| `guildScheduledEventUserAdd` / `...UserRemove` | `event \| null`, `user \| null`, `data` |
|
|
117
|
+
| `stageInstanceCreate` / `stageInstanceDelete` | `stageInstance` |
|
|
118
|
+
| `stageInstanceUpdate` | `oldStageInstance \| null`, `newStageInstance` |
|
|
119
|
+
| `guildSoundboardSoundCreate` | `sound` |
|
|
120
|
+
| `guildSoundboardSoundUpdate` | `oldSound \| null`, `newSound` |
|
|
121
|
+
| `guildSoundboardSoundDelete` | `sound \| null`, `data` |
|
|
122
|
+
| `guildSoundboardSoundsUpdate` / `soundboardSounds` | `sounds`, `guildId` |
|
|
123
|
+
| `guildBanAdd` / `guildBanRemove` | `ban` |
|
|
124
|
+
| `guildAuditLogEntryCreate` | `entry` |
|
|
125
|
+
| `autoModerationRuleCreate` / `autoModerationRuleDelete` | `rule` |
|
|
126
|
+
| `autoModerationRuleUpdate` | `oldRule \| null`, `newRule` |
|
|
127
|
+
| `autoModerationActionExecution` | `execution` |
|
|
128
|
+
| `guildIntegrationsUpdate` | `guild \| null`, `data` |
|
|
129
|
+
| `integrationCreate` | `integration` |
|
|
130
|
+
| `integrationUpdate` | `oldIntegration \| null`, `newIntegration` |
|
|
131
|
+
| `integrationDelete` | `integration \| null`, `data` |
|
|
132
|
+
|
|
133
|
+
The previous state of update events and the entity of delete events come from the cache, and are
|
|
134
|
+
`null` when it was not cached (or when the client has no cache). `data` is the raw dispatch data,
|
|
135
|
+
which always identifies the deleted entity.
|
|
136
|
+
|
|
137
|
+
`client.actions` holds an `Action` for each handled gateway dispatch. Each action captures the
|
|
138
|
+
previous state, then builds and emits events after the cache has been updated. The built-in actions
|
|
139
|
+
use the `DispatchHandlers` and `MultiDispatchHandlers` tables. Dispatches they do not cover are
|
|
140
|
+
still written to the cache and emitted as `raw`. `INTERACTION_CREATE` is handled by the HTTP endpoint.
|
|
141
|
+
|
|
142
|
+
REST operations use `client.core.api` from `@discordjs/core`. A few endpoints without a matching
|
|
143
|
+
core method (cursor-based message pins, guild creation from a template, and thread member queries
|
|
144
|
+
with extra parameters) use the same core client's underlying REST transport.
|
|
145
|
+
|
|
146
|
+
Dispatches of the same guild (or direct message channel) are processed in order, so an
|
|
147
|
+
asynchronous cache never reorders them, while different guilds proceed concurrently: a slow guild
|
|
148
|
+
does not hold the others back. Dispatches that belong to no guild, such as `READY` or `USER_UPDATE`,
|
|
149
|
+
wait for everything queued before them on their shard, and everything after them waits for them.
|
|
150
|
+
`client.queueStats` reports the pending dispatches, and one still running after `dispatchTimeout`
|
|
151
|
+
is reported as a `DispatchTimeoutError` through the `error` event, without being cancelled.
|
|
152
|
+
|
|
153
|
+
A cache failure (Redis down, corrupt value) is always reported through `error`. With the default
|
|
154
|
+
`cacheFailure: "skip"` the event is dropped, so listeners never see state the cache does not hold;
|
|
155
|
+
with `"emitUncached"` it is emitted anyway, built from the payload, with `null` as previous state. `READY` is the
|
|
156
|
+
exception: it is always emitted, since it sets `client.user` from the payload alone.
|
|
157
|
+
|
|
158
|
+
On `READY`, the cached guilds of that shard which `READY` no longer lists are dropped and emitted as
|
|
159
|
+
`guildDelete`: the bot left them while disconnected, or while the process was down with a
|
|
160
|
+
persistent cache, and Discord does not replay those removals. This is best effort: a failure (cache unreachable,
|
|
161
|
+
unknown shard count) is reported through `error` and keeps the remaining guilds.
|
|
162
|
+
|
|
163
|
+
## Listeners
|
|
164
|
+
|
|
165
|
+
Since `GatewayClient` emits on the client itself, gateway events are handled by regular listener
|
|
166
|
+
pieces. `EventGatewayListener` pins the emitter to the client and types `run` after the event:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// listeners/log-messages.ts
|
|
170
|
+
import { EventGatewayListener, type Message } from "@wolfstar/plugin-gateway";
|
|
171
|
+
|
|
172
|
+
export class LogMessagesListener extends EventGatewayListener<"messageCreate"> {
|
|
173
|
+
public constructor(context: EventGatewayListener.LoaderContext) {
|
|
174
|
+
super(context, { event: "messageCreate" });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public override run(message: Message) {
|
|
178
|
+
console.log(`${message.author.username}: ${message.content}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or, without a constructor, with `RegisterAsGatewayListener` (implemented like
|
|
184
|
+
`@wolfstar/plugin-subcommands-advanced`'s `RegisterAsSubcommand`):
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import {
|
|
188
|
+
EventGatewayListener,
|
|
189
|
+
RegisterAsGatewayListener,
|
|
190
|
+
type Message,
|
|
191
|
+
} from "@wolfstar/plugin-gateway";
|
|
192
|
+
|
|
193
|
+
@RegisterAsGatewayListener("messageCreate", { once: false })
|
|
194
|
+
export class LogMessagesListener extends EventGatewayListener<"messageCreate"> {
|
|
195
|
+
public override run(message: Message) {
|
|
196
|
+
console.log(`${message.author.username}: ${message.content}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
With `once: true`, the listener unloads itself after its first run.
|
|
202
|
+
|
|
203
|
+
## Caching
|
|
204
|
+
|
|
205
|
+
The cache only holds raw API data, managers build the structures:
|
|
206
|
+
|
|
207
|
+
Unlike the structure cache in `discordjs/next`, each cache read here builds a fresh structure. This
|
|
208
|
+
keeps the same behavior with in-memory and Redis stores and preserves the previous state of update
|
|
209
|
+
events.
|
|
210
|
+
|
|
211
|
+
| Manager | `get` / `fetch` / `refresh` arguments |
|
|
212
|
+
| ----------------- | ------------------------------------- |
|
|
213
|
+
| `client.users` | `userId` |
|
|
214
|
+
| `client.guilds` | `guildId` |
|
|
215
|
+
| `client.channels` | `channelId` (threads included) |
|
|
216
|
+
| `client.threads` | `threadId` |
|
|
217
|
+
| `client.messages` | `channelId`, `messageId` |
|
|
218
|
+
| `client.members` | `guildId`, `userId` |
|
|
219
|
+
| `client.roles` | `guildId`, `roleId` |
|
|
220
|
+
|
|
221
|
+
- `get` only reads the cache, resolving to `undefined` on a miss;
|
|
222
|
+
- `fetch` reads the cache, falling back to the REST API (and caching the result). Pass
|
|
223
|
+
`{ force: true }` after the IDs to always hit the API, `{ cache: false }` not to store the result:
|
|
224
|
+
`client.messages.fetch(channelId, messageId, { force: true })`;
|
|
225
|
+
- `refresh` is `fetch` with `{ force: true }`;
|
|
226
|
+
- `resolve` takes a structure (returned as is) or a cache key, like discord.js's `resolve`.
|
|
227
|
+
|
|
228
|
+
Like discord.js's `CachedManager#_add`, every API payload goes through the manager's `_add`, which
|
|
229
|
+
merges it into the cached entry (the fields a partial payload lacks keep their cached value) and
|
|
230
|
+
builds the structure. Relations are resolved from the cache too: `message.author` is the entry of
|
|
231
|
+
`client.users`, `message.member` the one of `client.members`, and the same goes for
|
|
232
|
+
`member.user`, `emoji.author`, `sticker.user`, and `invite.inviter`. Every structure of a guild
|
|
233
|
+
(channels, threads, members, roles, messages, emojis, stickers, invites) has `guild`, the cached
|
|
234
|
+
guild, and messages have `channel`. These are `null` when the entity is not cached; `fetchGuild()`
|
|
235
|
+
and `fetchChannel()` always get it. `_add` is asynchronous,
|
|
236
|
+
since the cache can be Redis. A structure built by hand, with `new Message(data)`, falls back to
|
|
237
|
+
the copy embedded in its payload.
|
|
238
|
+
|
|
239
|
+
Swapping `createInMemoryCache()` for `createRedisCache({ redis })` changes nothing else, see
|
|
240
|
+
[`@wolfstar/plugin-cache`](../plugin-cache).
|
|
241
|
+
|
|
242
|
+
## Structures
|
|
243
|
+
|
|
244
|
+
`User`, `Guild`, `Message`, `GuildMember`, and `Role` wrap the raw data behind typed getters.
|
|
245
|
+
Channels get one class per type (`TextChannel`, `VoiceChannel`, `ForumChannel`,
|
|
246
|
+
`PublicThreadChannel`, `DMChannel`, ...), all extending `Channel` and composed from mixins
|
|
247
|
+
(`GuildChannelMixin`, `ChannelTopicMixin`, `ThreadChannelMixin`, ...), following
|
|
248
|
+
`@discordjs/structures` and the layout of discord.js's `@discordjs/next` prototype.
|
|
249
|
+
`ChannelManager` picks the class matching the channel type, `BaseChannel` covers the unknown ones.
|
|
250
|
+
Channel mixins can supply a `DataTemplate`, an `optimizeData` hook, and an `enrichToJSON` hook.
|
|
251
|
+
Construction and patches optimize timestamps across channels, messages, members, invites, events,
|
|
252
|
+
templates, and voice states, as well as role and overwrite permission bits. `toJSON()` retains the
|
|
253
|
+
original API fields.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
client.on("channelCreate", (channel) => {
|
|
257
|
+
if (channel instanceof TextChannel) console.log(channel.name, channel.topic);
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Structures never hold a reference to the client. Every channel has `fetch()` and `delete()`
|
|
262
|
+
(from `BaseChannelMixin`), which use `@discordjs/core` for API calls.
|
|
263
|
+
|
|
264
|
+
`Structure`, `Mixin`, and the `kData`, `kPatch`, and `kClone` symbols are exported, so structures
|
|
265
|
+
can be subclassed and new mixins written:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { Mixin, TextChannel, kData } from "@wolfstar/plugin-gateway";
|
|
269
|
+
|
|
270
|
+
class MyTextChannel extends TextChannel {}
|
|
271
|
+
Mixin(MyTextChannel, [MyMixin]);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
> [!NOTE]
|
|
275
|
+
> `Structure` extends
|
|
276
|
+
> [`@discordjs/structures`](https://github.com/discordjs/discord.js/tree/main/packages/structures)'
|
|
277
|
+
> own base class. That package does not export the symbols keying a structure's data and its
|
|
278
|
+
> patch/clone methods, but creates them with `Symbol.for`, so `kData`, `kPatch`, and `kClone` are the
|
|
279
|
+
> very same symbols, re-exported for subclasses and mixins. It is only published as `dev` snapshots
|
|
280
|
+
> requiring Node.js 24.17 (hence this package's `engines`), and has no `Guild` nor `GuildMember`
|
|
281
|
+
> yet: the structures here are this package's own, following its conventions.
|
|
282
|
+
|
|
283
|
+
### Guilds, emojis, stickers and invites
|
|
284
|
+
|
|
285
|
+
`Guild` has every field of the API, its CDN URLs, and discord.js's editing methods (`edit`,
|
|
286
|
+
`setName`, `setIcon`, `setSystemChannel`, ..., `disableInvites`, `setIncidentActions`, `leave`,
|
|
287
|
+
`delete`), plus `fetchOwner`, `fetchPreview`, `fetchVanityData`, and `fetchVoiceRegions`. Its
|
|
288
|
+
emojis, stickers, and invites have their own managers, reachable from the guild or the client:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
const guild = await client.guilds.fetch(guildId);
|
|
292
|
+
|
|
293
|
+
const emoji = await guild.emojis.create({ attachment: "data:image/png;base64,...", name: "howl" });
|
|
294
|
+
await emoji.roles.add(roleId);
|
|
295
|
+
|
|
296
|
+
await client.guilds
|
|
297
|
+
.stickers(guildId)
|
|
298
|
+
.create({ file: { name: "wolf.png", data }, name: "wolf", tags: "wolf" });
|
|
299
|
+
|
|
300
|
+
const invite = await guild.invites.create(channelId, { maxAge: 3600 });
|
|
301
|
+
const fetched = await client.fetchInvite("https://discord.gg/wolves");
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The client also has discord.js's `fetchSticker`, `fetchStickerPacks`, and `fetchVoiceRegions`.
|
|
305
|
+
`emojiCreate`/`Update`/`Delete` and the sticker events come from diffing `GUILD_EMOJIS_UPDATE` and
|
|
306
|
+
`GUILD_STICKERS_UPDATE` against the cache, so a client without cache only gets them through `raw`.
|
|
307
|
+
|
|
308
|
+
### Users, members and roles
|
|
309
|
+
|
|
310
|
+
They follow discord.js's API, with one difference: anything discord.js reads synchronously from its
|
|
311
|
+
cache is asynchronous here, since the cache can be Redis.
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const member = await client.members.fetch(guildId, userId);
|
|
315
|
+
|
|
316
|
+
await member.roles.add(roleId, "verified");
|
|
317
|
+
await member.timeout(10 * 60_000, "spam");
|
|
318
|
+
|
|
319
|
+
const permissions = await member.fetchPermissions(); // discord.js: member.permissions
|
|
320
|
+
if (await member.fetchKickable()) await member.kick(); // discord.js: member.kickable
|
|
321
|
+
|
|
322
|
+
const highest = await member.roles.fetchHighest(); // discord.js: member.roles.highest
|
|
323
|
+
await highest?.setColors({ primaryColor: 0xff0000 });
|
|
324
|
+
|
|
325
|
+
await client.user?.setActivity("with wolves", { type: ActivityType.Competing });
|
|
326
|
+
await (await client.users.fetch(userId)).send("Welcome!");
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`client.user` is a `ClientUser`, which edits the bot's profile and sets its presence on every shard.
|
|
330
|
+
`client.members` also lists, searches, adds (OAuth2), edits, kicks, bans and prunes members;
|
|
331
|
+
`client.roles` creates, edits, moves and deletes roles, and fetches all of a guild's roles or their
|
|
332
|
+
member counts. Permissions are `PermissionsBitField`s, computed like Discord does: owner and
|
|
333
|
+
administrators get everything, everyone else `@everyone` plus their roles. Channel overwrites
|
|
334
|
+
apply through `member.fetchPermissionsIn(channel)`, see below.
|
|
335
|
+
|
|
336
|
+
### Channels and permissions
|
|
337
|
+
|
|
338
|
+
Guild channels follow discord.js: `edit`, `setName`, `clone`, `delete`, and the setters of each
|
|
339
|
+
type (`setTopic`, `setRateLimitPerUser`, `setBitrate`, `setUserLimit`, `setAvailableTags`, ...).
|
|
340
|
+
`setParent` and `lockPermissions` copy the category's overwrites. `channel.permissionOverwrites`
|
|
341
|
+
creates, edits (keeping the permissions you do not pass), and deletes overwrites, and
|
|
342
|
+
`guild.channels` creates, lists, and moves channels:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
const channel = await guild.channels.create({ name: "den", type: ChannelType.GuildText });
|
|
346
|
+
await channel.permissionOverwrites.edit(guild.id, { SendMessages: false });
|
|
347
|
+
await channel.permissionOverwrites.edit(roleId, { SendMessages: true });
|
|
348
|
+
|
|
349
|
+
const permissions = await channel.fetchPermissionsFor(member); // discord.js: channel.permissionsFor(member)
|
|
350
|
+
await member.fetchPermissionsIn(channel); // discord.js: member.permissionsIn(channel)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Permissions are computed like Discord does: guild permissions, then the `@everyone` overwrite, the
|
|
354
|
+
roles' overwrites, and the member's. Threads use their parent's overwrites. The message
|
|
355
|
+
`fetch*able()` checks use them too.
|
|
356
|
+
|
|
357
|
+
### Threads
|
|
358
|
+
|
|
359
|
+
Text, announcement, forum, and media channels have `threads`: `create` (a post with `message` in
|
|
360
|
+
forums), `fetchActive`, and `fetchArchived`. Threads have `setArchived`, `setLocked`,
|
|
361
|
+
`setInvitable`, `setAutoArchiveDuration`, `setAppliedTags`, `join`, `leave`,
|
|
362
|
+
`fetchStarterMessage`, `fetchOwner`, and `members`, backed by `client.threadMembers` and its
|
|
363
|
+
`ThreadMember`s. `threadListSync`, `threadMemberUpdate`, and `threadMembersUpdate` are emitted.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
const thread = await channel.threads.create({ name: "hunt", type: ChannelType.PrivateThread });
|
|
367
|
+
await thread.members.add(userId);
|
|
368
|
+
await thread.setArchived(true);
|
|
369
|
+
|
|
370
|
+
const post = await forum.threads.create({
|
|
371
|
+
name: "Pack news",
|
|
372
|
+
message: "Awoo",
|
|
373
|
+
appliedTags: [tagId],
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Voice states and presences
|
|
378
|
+
|
|
379
|
+
`client.voiceStates` and `client.presences` read the voice states and presences the gateway sends
|
|
380
|
+
(with the `GuildVoiceStates` and `GuildPresences` intents). `VoiceState` mutes, deafens, moves,
|
|
381
|
+
and disconnects members, and handles stage channels (`setSuppressed`, `setRequestToSpeak`).
|
|
382
|
+
`Presence` has the status and `Activity`s, with their `RichPresenceAssets` URLs. Members have
|
|
383
|
+
`fetchVoiceState()` and `fetchPresence()` (discord.js: `member.voice`, `member.presence`).
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
client.on("voiceStateUpdate", (oldState, newState) => {
|
|
387
|
+
if (!oldState?.channelId && newState.channelId)
|
|
388
|
+
console.log(newState.member?.displayName, "joined");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const voice = await member.fetchVoiceState();
|
|
392
|
+
await voice?.setChannel(afkChannelId, "idle");
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Moderation
|
|
396
|
+
|
|
397
|
+
`guild.bans` lists, fetches (with the reason, which the gateway does not send), creates, and
|
|
398
|
+
removes bans. `guild.fetchAuditLogs()` returns a page of `GuildAuditLogsEntry`s with their
|
|
399
|
+
executors, and `guild.autoModerationRules` manages `AutoModerationRule`s, whose setters
|
|
400
|
+
(`setKeywordFilter`, `setAllowList`, ...) keep the rest of the trigger:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
const { entries } = await guild.fetchAuditLogs({ type: AuditLogEvent.MemberBanAdd, limit: 10 });
|
|
404
|
+
for (const entry of entries) console.log(entry.executor?.username, entry.targetId, entry.reason);
|
|
405
|
+
|
|
406
|
+
const rule = await guild.autoModerationRules.fetch(ruleId);
|
|
407
|
+
await rule.setKeywordFilter(["awoo"]);
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Scheduled events, stages, and soundboard
|
|
411
|
+
|
|
412
|
+
`guild.scheduledEvents` creates, edits, and deletes `GuildScheduledEvent`s and fetches their
|
|
413
|
+
subscribers. Stage channels have `createStageInstance` and `fetchStageInstance` (a
|
|
414
|
+
`StageInstance`, managed by `guild.stageInstances`). `guild.soundboardSounds` uploads and edits
|
|
415
|
+
`SoundboardSound`s, `client.fetchDefaultSoundboardSounds()` lists Discord's own, and voice channels
|
|
416
|
+
play them with `sendSoundboardSound`.
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
const event = await guild.scheduledEvents.create({
|
|
420
|
+
name: "Full moon",
|
|
421
|
+
scheduledStartTime: Date.now() + 86_400_000,
|
|
422
|
+
scheduledEndTime: Date.now() + 90_000_000,
|
|
423
|
+
privacyLevel: GuildScheduledEventPrivacyLevel.GuildOnly,
|
|
424
|
+
entityType: GuildScheduledEventEntityType.External,
|
|
425
|
+
entityMetadata: { location: "The den" },
|
|
426
|
+
});
|
|
427
|
+
await event.setStatus(GuildScheduledEventStatus.Active);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Integrations, templates, welcome screen, widget, and onboarding
|
|
431
|
+
|
|
432
|
+
`guild.integrations` lists and removes `Integration`s, cached from the `INTEGRATION_*` dispatches.
|
|
433
|
+
`client.templates` manages `GuildTemplate`s (`guild.fetchTemplates()`, `guild.createTemplate()`,
|
|
434
|
+
`client.fetchGuildTemplate(code)`, `template.sync()`, `template.createGuild()`). Guilds fetch and
|
|
435
|
+
edit their `WelcomeScreen`, their widget settings (`setWidgetSettings` patches `widgetEnabled` and
|
|
436
|
+
`widgetChannelId`), and their `GuildOnboarding`, whose new prompts and options get placeholder IDs
|
|
437
|
+
like discord.js's. `client.fetchGuildWidget(guildId)` returns the public `Widget`. Apart from
|
|
438
|
+
integrations, Discord sends none of these over the gateway, so they are not cached.
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
await guild.editWelcomeScreen({
|
|
442
|
+
enabled: true,
|
|
443
|
+
welcomeChannels: [{ channel: rulesId, description: "Read me", emoji: "🐺" }],
|
|
444
|
+
});
|
|
445
|
+
const widget = await client.fetchGuildWidget(guild.id);
|
|
446
|
+
console.log(widget.presenceCount, widget.imageURL(GuildWidgetStyle.Banner2));
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Webhooks
|
|
450
|
+
|
|
451
|
+
`client.webhooks` fetches, creates, edits, and deletes webhooks, and posts with their token like
|
|
452
|
+
discord.js's `WebhookClient`. Text, announcement, voice, stage, forum, and media channels have
|
|
453
|
+
`fetchWebhooks` and `createWebhook`, guilds `fetchWebhooks`, and announcement channels
|
|
454
|
+
`addFollower`. Webhooks are not cached: Discord only says that they changed (`webhooksUpdate`).
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
const webhook = await channel.createWebhook({ name: "Howler" });
|
|
458
|
+
const message = await webhook.send({ content: "Awoo", username: "Pack" });
|
|
459
|
+
await webhook.editMessage(message.id, "Awoo!");
|
|
460
|
+
|
|
461
|
+
const fetched = await client.fetchWebhook(webhookId, token); // no bot authorization needed
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Messages
|
|
465
|
+
|
|
466
|
+
`Message` follows discord.js: `attachments`, `embeds`, `mentions` (`MessageMentions`), `reactions`
|
|
467
|
+
(`ReactionManager`), `poll` (`Poll`), `flags`, `cleanContent`, and the actions `reply`, `edit`,
|
|
468
|
+
`delete`, `forward`, `pin`, `react`, `crosspost`, `startThread`, `suppressEmbeds`. Relations are
|
|
469
|
+
fetched: `fetchChannel`, `fetchGuild`, `fetchReference`, and `fetchDeletable` & co. instead of
|
|
470
|
+
discord.js's `deletable`. Text channels get `messages`, `send`, `sendTyping`, and `bulkDelete`:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
const channel = await client.channels.fetch(channelId);
|
|
474
|
+
if (channel instanceof TextChannel) {
|
|
475
|
+
await channel.sendTyping();
|
|
476
|
+
const message = await channel.send({
|
|
477
|
+
content: "Awoo",
|
|
478
|
+
poll: { question: { text: "Best pack?" }, answers },
|
|
479
|
+
});
|
|
480
|
+
await message.react("🐺");
|
|
481
|
+
const voters = await message.poll?.answers[0]?.fetchVoters();
|
|
482
|
+
await channel.bulkDelete(10, true);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const { items } = await client.messages.fetchPins(channelId);
|
|
486
|
+
const users = await message.reactions.resolve("🐺")?.users.fetch();
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Subpath exports
|
|
490
|
+
|
|
491
|
+
Like `@discordjs/next`, the gateway and REST libraries are re-exported, so a bot does not need to
|
|
492
|
+
depend on them directly:
|
|
493
|
+
|
|
494
|
+
| Import | Re-exports |
|
|
495
|
+
| ------------------------------- | ----------------- |
|
|
496
|
+
| `@wolfstar/plugin-gateway/rest` | `@discordjs/rest` |
|
|
497
|
+
| `@wolfstar/plugin-gateway/ws` | `@discordjs/ws` |
|
|
498
|
+
|
|
499
|
+
## Limitations
|
|
500
|
+
|
|
501
|
+
- A `GatewayClient` connects its gateway shards from a single process (`@discordjs/ws`'s
|
|
502
|
+
`WorkerShardingStrategy` can be set through `gateway.buildStrategy`). To spread them across
|
|
503
|
+
processes, use [`@wolfstar/plugin-sharder`](../plugin-sharder) and spread
|
|
504
|
+
`shardClient.gatewayOptions` into the client's options.
|
|
505
|
+
- Interaction payloads keep being handled as today, they do not read through `client.users` & co.
|