@dereekb/nestjs 13.1.0 → 13.2.1

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.
@@ -0,0 +1,475 @@
1
+ import { handlerFactory, handlerConfigurerFactory, handlerMappedSetFunctionFactory, lastValue } from '@dereekb/util';
2
+ import { InteractionType, GatewayIntentBits, Client, TextChannel, Events } from 'discord.js';
3
+ import { RawBody } from '@dereekb/nestjs';
4
+ import { Injectable, Inject, Logger, Post, Req, Controller, Module } from '@nestjs/common';
5
+ import { createPublicKey, verify } from 'crypto';
6
+ import { ConfigService, ConfigModule } from '@nestjs/config';
7
+ import { fetchPageFactory } from '@dereekb/util/fetch';
8
+
9
+ /**
10
+ * Casts an untyped Discord interaction to a typed one.
11
+ *
12
+ * @param interaction - the raw interaction to cast
13
+ */
14
+ function discordWebhookInteraction(interaction) {
15
+ return interaction;
16
+ }
17
+ const discordInteractionHandlerFactory = handlerFactory((x) => x.type);
18
+ const discordInteractionHandlerConfigurerFactory = handlerConfigurerFactory({
19
+ configurerForAccessor: (accessor) => {
20
+ // eslint-disable-next-line
21
+ const fnWithKey = handlerMappedSetFunctionFactory(accessor, discordWebhookInteraction);
22
+ const configurer = {
23
+ ...accessor,
24
+ handleApplicationCommand: fnWithKey(InteractionType.ApplicationCommand),
25
+ handleMessageComponent: fnWithKey(InteractionType.MessageComponent),
26
+ handleModalSubmit: fnWithKey(InteractionType.ModalSubmit),
27
+ handleAutocomplete: fnWithKey(InteractionType.ApplicationCommandAutocomplete)
28
+ };
29
+ return configurer;
30
+ }
31
+ });
32
+
33
+ /**
34
+ * Default environment variable for the Discord application public key.
35
+ */
36
+ const DISCORD_PUBLIC_KEY_ENV_VAR = 'DISCORD_PUBLIC_KEY';
37
+ /**
38
+ * Configuration for the DiscordWebhookService.
39
+ */
40
+ class DiscordWebhookServiceConfig {
41
+ discordWebhook;
42
+ static assertValidConfig(config) {
43
+ if (!config.discordWebhook.publicKey) {
44
+ throw new Error('No Discord public key specified.');
45
+ }
46
+ }
47
+ }
48
+
49
+ /******************************************************************************
50
+ Copyright (c) Microsoft Corporation.
51
+
52
+ Permission to use, copy, modify, and/or distribute this software for any
53
+ purpose with or without fee is hereby granted.
54
+
55
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
56
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
57
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
58
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
59
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
60
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
61
+ PERFORMANCE OF THIS SOFTWARE.
62
+ ***************************************************************************** */
63
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
64
+
65
+
66
+ function __decorate(decorators, target, key, desc) {
67
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
68
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
69
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
70
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
71
+ }
72
+
73
+ function __param(paramIndex, decorator) {
74
+ return function (target, key) { decorator(target, key, paramIndex); }
75
+ }
76
+
77
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
78
+ var e = new Error(message);
79
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
80
+ };
81
+
82
+ /**
83
+ * Creates a verifier for Discord interaction webhook requests.
84
+ *
85
+ * Discord signs interaction webhook requests with Ed25519. The signed message is
86
+ * the concatenation of the x-signature-timestamp header and the raw request body.
87
+ * The signature is provided in the x-signature-ed25519 header as a hex string.
88
+ *
89
+ * Uses Node.js built-in crypto with JWK key import — no external dependencies required.
90
+ *
91
+ * @param config - verification config containing the application's public key
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * const verifier = discordWebhookEventVerifier({ publicKey: 'your-hex-public-key' });
96
+ * const result = await verifier(req, rawBody);
97
+ *
98
+ * if (result.valid) {
99
+ * // result.body contains the parsed interaction
100
+ * }
101
+ * ```
102
+ */
103
+ function discordWebhookEventVerifier(config) {
104
+ const { publicKey: publicKeyHex } = config;
105
+ // Import the raw 32-byte Ed25519 public key via JWK format.
106
+ const publicKey = createPublicKey({
107
+ key: {
108
+ kty: 'OKP',
109
+ crv: 'Ed25519',
110
+ x: Buffer.from(publicKeyHex, 'hex').toString('base64url')
111
+ },
112
+ format: 'jwk'
113
+ });
114
+ return async (request, rawBody) => {
115
+ const signature = request.headers['x-signature-ed25519'];
116
+ const timestamp = request.headers['x-signature-timestamp'];
117
+ let result;
118
+ if (!signature || !timestamp) {
119
+ result = { valid: false };
120
+ }
121
+ else {
122
+ const message = Buffer.concat([Buffer.from(timestamp), rawBody]);
123
+ const signatureBuffer = Buffer.from(signature, 'hex');
124
+ let valid = false;
125
+ try {
126
+ valid = verify(null, message, publicKey, signatureBuffer);
127
+ }
128
+ catch {
129
+ valid = false;
130
+ }
131
+ if (valid) {
132
+ const body = JSON.parse(rawBody.toString('utf-8'));
133
+ result = { valid: true, body };
134
+ }
135
+ else {
136
+ result = { valid: false };
137
+ }
138
+ }
139
+ return result;
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Service that handles Discord interaction webhook events.
145
+ *
146
+ * Verifies incoming webhook signatures and dispatches interactions to registered handlers.
147
+ */
148
+ let DiscordWebhookService = class DiscordWebhookService {
149
+ logger = new Logger('DiscordWebhookService');
150
+ _verifier;
151
+ handler = discordInteractionHandlerFactory();
152
+ configure = discordInteractionHandlerConfigurerFactory(this.handler);
153
+ constructor(discordWebhookServiceConfig) {
154
+ this._verifier = discordWebhookEventVerifier({
155
+ publicKey: discordWebhookServiceConfig.discordWebhook.publicKey
156
+ });
157
+ }
158
+ async updateForWebhook(req, rawBody) {
159
+ const result = await this._verifier(req, rawBody);
160
+ if (!result.valid) {
161
+ this.logger.warn('Received invalid Discord interaction event.', req);
162
+ }
163
+ else {
164
+ await this.updateForDiscordInteraction(result.body);
165
+ }
166
+ }
167
+ async updateForDiscordInteraction(interaction) {
168
+ const result = await this.handler(interaction);
169
+ if (!result) {
170
+ this.logger.warn('Received unexpected/unhandled Discord interaction.', interaction);
171
+ }
172
+ }
173
+ };
174
+ DiscordWebhookService = __decorate([
175
+ Injectable(),
176
+ __param(0, Inject(DiscordWebhookServiceConfig))
177
+ ], DiscordWebhookService);
178
+
179
+ let DiscordWebhookController = class DiscordWebhookController {
180
+ _discordWebhookService;
181
+ constructor(discordWebhookService) {
182
+ this._discordWebhookService = discordWebhookService;
183
+ }
184
+ async handleDiscordWebhook(req, rawBody) {
185
+ await this._discordWebhookService.updateForWebhook(req, rawBody);
186
+ }
187
+ };
188
+ __decorate([
189
+ Post(),
190
+ __param(0, Req()),
191
+ __param(1, RawBody())
192
+ ], DiscordWebhookController.prototype, "handleDiscordWebhook", null);
193
+ DiscordWebhookController = __decorate([
194
+ Controller('/webhook/discord'),
195
+ __param(0, Inject(DiscordWebhookService))
196
+ ], DiscordWebhookController);
197
+
198
+ /**
199
+ * Factory that creates a DiscordWebhookServiceConfig from environment variables.
200
+ */
201
+ function discordWebhookServiceConfigFactory(configService) {
202
+ const config = {
203
+ discordWebhook: {
204
+ publicKey: configService.get(DISCORD_PUBLIC_KEY_ENV_VAR)
205
+ }
206
+ };
207
+ DiscordWebhookServiceConfig.assertValidConfig(config);
208
+ return config;
209
+ }
210
+ /**
211
+ * NestJS module that provides Discord interaction webhook handling.
212
+ *
213
+ * Standalone — does not depend on DiscordModule (no bot token needed).
214
+ * Reads the application public key from the DISCORD_PUBLIC_KEY environment variable.
215
+ */
216
+ let DiscordWebhookModule = class DiscordWebhookModule {
217
+ };
218
+ DiscordWebhookModule = __decorate([
219
+ Module({
220
+ imports: [ConfigModule],
221
+ controllers: [DiscordWebhookController],
222
+ providers: [
223
+ {
224
+ provide: DiscordWebhookServiceConfig,
225
+ inject: [ConfigService],
226
+ useFactory: discordWebhookServiceConfigFactory
227
+ },
228
+ DiscordWebhookService
229
+ ],
230
+ exports: [DiscordWebhookService]
231
+ })
232
+ ], DiscordWebhookModule);
233
+
234
+ /**
235
+ * Default environment variable for the Discord bot token.
236
+ */
237
+ const DISCORD_BOT_TOKEN_ENV_VAR = 'DISCORD_BOT_TOKEN';
238
+ /**
239
+ * Default gateway intents for a bot that reads guild messages.
240
+ *
241
+ * Includes Guilds, GuildMessages, and MessageContent.
242
+ * Note: MessageContent is a privileged intent and must be enabled in the Discord Developer Portal.
243
+ */
244
+ const DISCORD_DEFAULT_INTENTS = [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent];
245
+ /**
246
+ * Configuration for the DiscordApi service.
247
+ */
248
+ class DiscordServiceConfig {
249
+ discord;
250
+ static assertValidConfig(config) {
251
+ if (!config.discord.botToken) {
252
+ throw new Error('No Discord bot token specified.');
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Injectable service that wraps the discord.js Client for bot operations.
259
+ *
260
+ * Automatically logs in on module init and destroys the client on module destroy
261
+ * when autoLogin is enabled (default).
262
+ */
263
+ let DiscordApi = class DiscordApi {
264
+ config;
265
+ logger = new Logger('DiscordApi');
266
+ /**
267
+ * The underlying discord.js Client instance.
268
+ */
269
+ client;
270
+ constructor(config) {
271
+ this.config = config;
272
+ const { clientOptions } = config.discord;
273
+ this.client = new Client({
274
+ intents: DISCORD_DEFAULT_INTENTS,
275
+ ...clientOptions
276
+ });
277
+ }
278
+ async onModuleInit() {
279
+ const { autoLogin = true, botToken } = this.config.discord;
280
+ let result;
281
+ if (autoLogin) {
282
+ result = this.client
283
+ .login(botToken)
284
+ .then(() => { })
285
+ .catch((e) => {
286
+ this.logger.error('Failed to log in to Discord', e);
287
+ });
288
+ }
289
+ else {
290
+ result = Promise.resolve();
291
+ }
292
+ return result;
293
+ }
294
+ async onModuleDestroy() {
295
+ return this.client.destroy();
296
+ }
297
+ /**
298
+ * Sends a text message to a Discord channel.
299
+ *
300
+ * @param channelId - target channel's snowflake ID
301
+ * @param content - message text to send
302
+ *
303
+ * @throws {Error} When the channel is not found or is not a text channel.
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * const message = await discordApi.sendMessage('123456789', 'Hello from the bot!');
308
+ * ```
309
+ */
310
+ async sendMessage(channelId, content) {
311
+ const channel = await this.client.channels.fetch(channelId);
312
+ if (!channel || !(channel instanceof TextChannel)) {
313
+ throw new Error(`Channel ${channelId} not found or is not a text channel.`);
314
+ }
315
+ return channel.send(content);
316
+ }
317
+ /**
318
+ * Registers a handler for the MessageCreate event (incoming messages).
319
+ *
320
+ * Returns an unsubscribe function to remove the handler.
321
+ *
322
+ * @param handler - callback invoked for each incoming message
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * const unsubscribe = discordApi.onMessage((message) => {
327
+ * if (!message.author.bot) {
328
+ * console.log(`${message.author.tag}: ${message.content}`);
329
+ * }
330
+ * });
331
+ *
332
+ * // Later, to stop listening:
333
+ * unsubscribe();
334
+ * ```
335
+ */
336
+ onMessage(handler) {
337
+ this.client.on(Events.MessageCreate, handler);
338
+ return () => this.client.off(Events.MessageCreate, handler);
339
+ }
340
+ };
341
+ DiscordApi = __decorate([
342
+ Injectable(),
343
+ __param(0, Inject(DiscordServiceConfig))
344
+ ], DiscordApi);
345
+
346
+ /**
347
+ * Factory that creates a DiscordServiceConfig from environment variables.
348
+ */
349
+ function discordServiceConfigFactory(configService) {
350
+ const config = {
351
+ discord: {
352
+ botToken: configService.get(DISCORD_BOT_TOKEN_ENV_VAR),
353
+ autoLogin: true
354
+ }
355
+ };
356
+ DiscordServiceConfig.assertValidConfig(config);
357
+ return config;
358
+ }
359
+ /**
360
+ * NestJS module that provides the DiscordApi service.
361
+ *
362
+ * Reads the bot token from the DISCORD_BOT_TOKEN environment variable.
363
+ */
364
+ let DiscordModule = class DiscordModule {
365
+ };
366
+ DiscordModule = __decorate([
367
+ Module({
368
+ imports: [ConfigModule],
369
+ providers: [
370
+ {
371
+ provide: DiscordServiceConfig,
372
+ inject: [ConfigService],
373
+ useFactory: discordServiceConfigFactory
374
+ },
375
+ DiscordApi
376
+ ],
377
+ exports: [DiscordApi]
378
+ })
379
+ ], DiscordModule);
380
+
381
+ /**
382
+ * Default number of messages per page when fetching Discord channel messages.
383
+ */
384
+ const DISCORD_DEFAULT_MESSAGES_PER_PAGE = 100;
385
+ /**
386
+ * Creates a page factory that wraps a Discord message fetch function with automatic cursor-based pagination.
387
+ *
388
+ * Discord paginates via `before`/`after` snowflake IDs. This factory automatically reads the last
389
+ * message's ID from each response and sets it as the `before` cursor for the next request.
390
+ * When the number of returned messages is less than the requested limit, pagination stops.
391
+ *
392
+ * @param fetch - The Discord fetch function to paginate over
393
+ * @param config - Optional config for reading message IDs
394
+ * @param defaults - Optional default configuration for the page factory
395
+ * @returns A page factory that produces iterable page fetchers
396
+ *
397
+ * @example
398
+ * ```typescript
399
+ * const pageFactory = discordFetchMessagePageFactory(fetchChannelMessages);
400
+ *
401
+ * const fetchPage = pageFactory({ limit: 50 });
402
+ * const firstPage = await fetchPage.fetchNext();
403
+ *
404
+ * if (firstPage.hasNext) {
405
+ * const secondPage = await firstPage.fetchNext();
406
+ * }
407
+ * ```
408
+ */
409
+ function discordFetchMessagePageFactory(fetch, config, defaults) {
410
+ const readMessageId = config?.readMessageId ?? ((message) => message.id);
411
+ return fetchPageFactory({
412
+ ...defaults,
413
+ fetch,
414
+ readFetchPageResultInfo(result) {
415
+ const count = result.data.length;
416
+ const lastMessage = lastValue(result.data);
417
+ const nextCursor = lastMessage ? readMessageId(lastMessage) : undefined;
418
+ return {
419
+ hasNext: count > 0,
420
+ nextPageCursor: nextCursor
421
+ };
422
+ },
423
+ buildInputForNextPage(pageResult, input, options) {
424
+ const nextCursor = pageResult.nextPageCursor;
425
+ const effectiveLimit = options.maxItemsPerPage ?? input.limit ?? DISCORD_DEFAULT_MESSAGES_PER_PAGE;
426
+ const resultCount = pageResult.result?.data.length ?? 0;
427
+ // Discord signals no more results when fewer items than the limit are returned
428
+ if (!nextCursor || resultCount < effectiveLimit) {
429
+ return undefined;
430
+ }
431
+ return {
432
+ ...input,
433
+ before: nextCursor,
434
+ after: undefined,
435
+ around: undefined,
436
+ limit: effectiveLimit
437
+ };
438
+ }
439
+ });
440
+ }
441
+
442
+ /**
443
+ * Returns default ClientOptions for a bot that reads guild messages.
444
+ *
445
+ * Includes Guilds, GuildMessages, and MessageContent intents.
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * const options = discordDefaultClientOptions();
450
+ * // options.intents === [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent]
451
+ * ```
452
+ */
453
+ function discordDefaultClientOptions() {
454
+ return {
455
+ intents: DISCORD_DEFAULT_INTENTS
456
+ };
457
+ }
458
+ /**
459
+ * Returns ClientOptions with additional intents merged with the defaults.
460
+ *
461
+ * @param additionalIntents - extra intents to include beyond the defaults
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * const options = discordClientOptionsWithIntents([GatewayIntentBits.DirectMessages]);
466
+ * // options.intents includes Guilds, GuildMessages, MessageContent, and DirectMessages
467
+ * ```
468
+ */
469
+ function discordClientOptionsWithIntents(additionalIntents) {
470
+ return {
471
+ intents: [...DISCORD_DEFAULT_INTENTS, ...additionalIntents]
472
+ };
473
+ }
474
+
475
+ export { DISCORD_BOT_TOKEN_ENV_VAR, DISCORD_DEFAULT_INTENTS, DISCORD_DEFAULT_MESSAGES_PER_PAGE, DISCORD_PUBLIC_KEY_ENV_VAR, DiscordApi, DiscordModule, DiscordServiceConfig, DiscordWebhookController, DiscordWebhookModule, DiscordWebhookService, DiscordWebhookServiceConfig, discordClientOptionsWithIntents, discordDefaultClientOptions, discordFetchMessagePageFactory, discordInteractionHandlerConfigurerFactory, discordInteractionHandlerFactory, discordServiceConfigFactory, discordWebhookEventVerifier, discordWebhookInteraction, discordWebhookServiceConfigFactory };
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@dereekb/nestjs/discord",
3
+ "version": "13.2.1",
4
+ "peerDependencies": {
5
+ "@dereekb/nestjs": "13.2.1",
6
+ "@dereekb/util": "13.2.1",
7
+ "@nestjs/common": "^11.0.0",
8
+ "@nestjs/config": "^4.0.0",
9
+ "discord.js": "^14.25.1",
10
+ "express": "^5.0.0"
11
+ },
12
+ "exports": {
13
+ "./package.json": "./package.json",
14
+ ".": {
15
+ "module": "./index.esm.js",
16
+ "types": "./index.d.ts",
17
+ "import": "./index.cjs.mjs",
18
+ "default": "./index.cjs.js"
19
+ }
20
+ },
21
+ "module": "./index.esm.js",
22
+ "main": "./index.cjs.js",
23
+ "types": "./index.d.ts"
24
+ }
@@ -0,0 +1 @@
1
+ export * from './lib';
@@ -0,0 +1,55 @@
1
+ import { Client, type Message } from 'discord.js';
2
+ import { type OnModuleDestroy, type OnModuleInit } from '@nestjs/common';
3
+ import { DiscordServiceConfig } from './discord.config';
4
+ import { type DiscordChannelId } from './discord.type';
5
+ /**
6
+ * Injectable service that wraps the discord.js Client for bot operations.
7
+ *
8
+ * Automatically logs in on module init and destroys the client on module destroy
9
+ * when autoLogin is enabled (default).
10
+ */
11
+ export declare class DiscordApi implements OnModuleInit, OnModuleDestroy {
12
+ readonly config: DiscordServiceConfig;
13
+ private readonly logger;
14
+ /**
15
+ * The underlying discord.js Client instance.
16
+ */
17
+ readonly client: Client;
18
+ constructor(config: DiscordServiceConfig);
19
+ onModuleInit(): Promise<void>;
20
+ onModuleDestroy(): Promise<void>;
21
+ /**
22
+ * Sends a text message to a Discord channel.
23
+ *
24
+ * @param channelId - target channel's snowflake ID
25
+ * @param content - message text to send
26
+ *
27
+ * @throws {Error} When the channel is not found or is not a text channel.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const message = await discordApi.sendMessage('123456789', 'Hello from the bot!');
32
+ * ```
33
+ */
34
+ sendMessage(channelId: DiscordChannelId, content: string): Promise<Message>;
35
+ /**
36
+ * Registers a handler for the MessageCreate event (incoming messages).
37
+ *
38
+ * Returns an unsubscribe function to remove the handler.
39
+ *
40
+ * @param handler - callback invoked for each incoming message
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const unsubscribe = discordApi.onMessage((message) => {
45
+ * if (!message.author.bot) {
46
+ * console.log(`${message.author.tag}: ${message.content}`);
47
+ * }
48
+ * });
49
+ *
50
+ * // Later, to stop listening:
51
+ * unsubscribe();
52
+ * ```
53
+ */
54
+ onMessage(handler: (message: Message) => void): () => void;
55
+ }
@@ -0,0 +1,89 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type FetchPageFactory, type FetchPageFactoryConfigDefaults } from '@dereekb/util/fetch';
3
+ import { type DiscordMessageId } from './discord.type';
4
+ /**
5
+ * Default number of messages per page when fetching Discord channel messages.
6
+ */
7
+ export declare const DISCORD_DEFAULT_MESSAGES_PER_PAGE = 100;
8
+ /**
9
+ * Base pagination parameters for Discord channel message endpoints.
10
+ *
11
+ * Discord uses cursor-based pagination via snowflake IDs rather than page numbers.
12
+ * Only one of `before`, `after`, or `around` should be specified per request.
13
+ */
14
+ export interface DiscordMessagePageFilter {
15
+ /**
16
+ * Fetch messages before this message ID.
17
+ */
18
+ readonly before?: Maybe<DiscordMessageId>;
19
+ /**
20
+ * Fetch messages after this message ID.
21
+ */
22
+ readonly after?: Maybe<DiscordMessageId>;
23
+ /**
24
+ * Fetch messages around this message ID.
25
+ */
26
+ readonly around?: Maybe<DiscordMessageId>;
27
+ /**
28
+ * Maximum number of messages to return per page (1-100).
29
+ *
30
+ * Defaults to {@link DISCORD_DEFAULT_MESSAGES_PER_PAGE}.
31
+ */
32
+ readonly limit?: Maybe<number>;
33
+ }
34
+ /**
35
+ * Result of a paginated Discord message fetch containing the array of messages.
36
+ *
37
+ * @typeParam T - The message type (typically discord.js `Message`)
38
+ */
39
+ export interface DiscordMessagePageResult<T> {
40
+ /**
41
+ * Array of messages returned.
42
+ */
43
+ readonly data: T[];
44
+ }
45
+ /**
46
+ * A fetch function that accepts {@link DiscordMessagePageFilter} input and returns a {@link DiscordMessagePageResult}.
47
+ * Used as the underlying data source for {@link discordFetchMessagePageFactory}.
48
+ */
49
+ export type DiscordFetchMessagePageFetchFunction<I extends DiscordMessagePageFilter, T> = (input: I) => Promise<DiscordMessagePageResult<T>>;
50
+ /**
51
+ * Configuration for {@link discordFetchMessagePageFactory}.
52
+ *
53
+ * @typeParam T - The message type
54
+ */
55
+ export interface DiscordFetchMessagePageFactoryConfig<T> {
56
+ /**
57
+ * Extracts the snowflake ID from a message object. Used to determine the cursor for the next page.
58
+ *
59
+ * Defaults to reading the `id` property on the message.
60
+ */
61
+ readonly readMessageId?: (message: T) => DiscordMessageId;
62
+ }
63
+ /**
64
+ * Creates a page factory that wraps a Discord message fetch function with automatic cursor-based pagination.
65
+ *
66
+ * Discord paginates via `before`/`after` snowflake IDs. This factory automatically reads the last
67
+ * message's ID from each response and sets it as the `before` cursor for the next request.
68
+ * When the number of returned messages is less than the requested limit, pagination stops.
69
+ *
70
+ * @param fetch - The Discord fetch function to paginate over
71
+ * @param config - Optional config for reading message IDs
72
+ * @param defaults - Optional default configuration for the page factory
73
+ * @returns A page factory that produces iterable page fetchers
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const pageFactory = discordFetchMessagePageFactory(fetchChannelMessages);
78
+ *
79
+ * const fetchPage = pageFactory({ limit: 50 });
80
+ * const firstPage = await fetchPage.fetchNext();
81
+ *
82
+ * if (firstPage.hasNext) {
83
+ * const secondPage = await firstPage.fetchNext();
84
+ * }
85
+ * ```
86
+ */
87
+ export declare function discordFetchMessagePageFactory<I extends DiscordMessagePageFilter, T extends {
88
+ id: string;
89
+ }>(fetch: DiscordFetchMessagePageFetchFunction<I, T>, config?: Maybe<DiscordFetchMessagePageFactoryConfig<T>>, defaults?: Maybe<FetchPageFactoryConfigDefaults>): FetchPageFactory<I, DiscordMessagePageResult<T>>;
@@ -0,0 +1,36 @@
1
+ import { type ClientOptions, GatewayIntentBits } from 'discord.js';
2
+ import { type DiscordBotToken } from './discord.type';
3
+ /**
4
+ * Default environment variable for the Discord bot token.
5
+ */
6
+ export declare const DISCORD_BOT_TOKEN_ENV_VAR = "DISCORD_BOT_TOKEN";
7
+ /**
8
+ * Default gateway intents for a bot that reads guild messages.
9
+ *
10
+ * Includes Guilds, GuildMessages, and MessageContent.
11
+ * Note: MessageContent is a privileged intent and must be enabled in the Discord Developer Portal.
12
+ */
13
+ export declare const DISCORD_DEFAULT_INTENTS: GatewayIntentBits[];
14
+ export interface DiscordServiceApiConfig {
15
+ /**
16
+ * The bot token used to authenticate with the Discord gateway.
17
+ */
18
+ readonly botToken: DiscordBotToken;
19
+ /**
20
+ * discord.js Client options. Intents default to DISCORD_DEFAULT_INTENTS if not provided.
21
+ */
22
+ readonly clientOptions?: Partial<ClientOptions>;
23
+ /**
24
+ * Whether to automatically call client.login() during module initialization.
25
+ *
26
+ * Defaults to true.
27
+ */
28
+ readonly autoLogin?: boolean;
29
+ }
30
+ /**
31
+ * Configuration for the DiscordApi service.
32
+ */
33
+ export declare abstract class DiscordServiceConfig {
34
+ readonly discord: DiscordServiceApiConfig;
35
+ static assertValidConfig(config: DiscordServiceConfig): void;
36
+ }
@@ -0,0 +1,13 @@
1
+ import { ConfigService } from '@nestjs/config';
2
+ import { DiscordServiceConfig } from './discord.config';
3
+ /**
4
+ * Factory that creates a DiscordServiceConfig from environment variables.
5
+ */
6
+ export declare function discordServiceConfigFactory(configService: ConfigService): DiscordServiceConfig;
7
+ /**
8
+ * NestJS module that provides the DiscordApi service.
9
+ *
10
+ * Reads the bot token from the DISCORD_BOT_TOKEN environment variable.
11
+ */
12
+ export declare class DiscordModule {
13
+ }