@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/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
+ [![version](https://npmx.dev/api/registry/badge/version/@wolfstar/plugin-gateway)](https://npmx.dev/package/@wolfstar/plugin-gateway)
10
+ [![downloads](https://npmx.dev/api/registry/badge/downloads/@wolfstar/plugin-gateway)](https://npmx.dev/package/@wolfstar/plugin-gateway)
11
+ [![license](https://img.shields.io/github/license/wolfstar-project/plugins?style=flat-square&color=informational)](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.