@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.
- package/discord/index.cjs.default.js +1 -0
- package/discord/index.cjs.js +491 -0
- package/discord/index.cjs.mjs +2 -0
- package/discord/index.d.ts +1 -0
- package/discord/index.esm.js +475 -0
- package/discord/package.json +24 -0
- package/discord/src/index.d.ts +1 -0
- package/discord/src/lib/discord.api.d.ts +55 -0
- package/discord/src/lib/discord.api.page.d.ts +89 -0
- package/discord/src/lib/discord.config.d.ts +36 -0
- package/discord/src/lib/discord.module.d.ts +13 -0
- package/discord/src/lib/discord.type.d.ts +22 -0
- package/discord/src/lib/discord.util.d.ts +25 -0
- package/discord/src/lib/index.d.ts +7 -0
- package/discord/src/lib/webhook/index.d.ts +6 -0
- package/discord/src/lib/webhook/webhook.discord.config.d.ts +18 -0
- package/discord/src/lib/webhook/webhook.discord.controller.d.ts +8 -0
- package/discord/src/lib/webhook/webhook.discord.d.ts +58 -0
- package/discord/src/lib/webhook/webhook.discord.module.d.ts +14 -0
- package/discord/src/lib/webhook/webhook.discord.service.d.ts +18 -0
- package/discord/src/lib/webhook/webhook.discord.verify.d.ts +45 -0
- package/mailgun/package.json +6 -6
- package/openai/package.json +6 -6
- package/package.json +9 -2
- package/stripe/package.json +6 -6
- package/typeform/package.json +6 -6
- package/vapiai/package.json +6 -6
|
@@ -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
|
+
}
|