@scotthamilton77/discord-bot-lib 0.1.2 → 1.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 +21 -26
- package/dist/index.cjs +100 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -21
- package/dist/index.d.ts +20 -21
- package/dist/index.js +100 -108
- package/dist/index.js.map +1 -1
- package/package.json +2 -7
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ TypeScript library for managing Discord bot identities, messaging, and multi-bot
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
12
|
- **Factory construction** -- `Bot.fromConfig()` connects, verifies guilds, and returns a ready bot in one call
|
|
13
|
-
- **
|
|
14
|
-
- **Multi-bot management** -- `ConnectorManager` aggregates
|
|
13
|
+
- **Unified receive API** -- event-driven callback (`onMessage`) and async iterable (`messages()`) with full routing metadata (`isDM`, `isMention`, `isReply`, `replyTo`)
|
|
14
|
+
- **Multi-bot management** -- `ConnectorManager` aggregates messages across bots with lifecycle events
|
|
15
15
|
- **Guided onboarding** -- `BotOnboarding` walks through token validation, server invitation, and permission verification
|
|
16
16
|
- **Message chunking** -- automatically splits messages at paragraph/line/word boundaries to stay within Discord's 2000-char limit
|
|
17
17
|
- **File attachments** -- send files from Buffer or file path, with size validation and filename sanitization
|
|
@@ -37,19 +37,14 @@ const bot = await Bot.fromConfig({
|
|
|
37
37
|
token: process.env.DISCORD_TOKEN!,
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
bot.
|
|
42
|
-
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
bot.
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Listen to all messages in a specific channel
|
|
51
|
-
bot.onMessage("123456789012345678", (event) => {
|
|
52
|
-
console.log(`${event.author.username}: ${event.content}`);
|
|
40
|
+
// Every incoming message — route by metadata
|
|
41
|
+
bot.onMessage(async (event) => {
|
|
42
|
+
if (event.isMention) {
|
|
43
|
+
await bot.send(event.channelId, `Hello, ${event.author.username}!`);
|
|
44
|
+
}
|
|
45
|
+
if (event.isReply) {
|
|
46
|
+
await bot.send(event.channelId, `You replied to my message ${event.replyTo!.messageId}`);
|
|
47
|
+
}
|
|
53
48
|
});
|
|
54
49
|
|
|
55
50
|
// Error handling
|
|
@@ -58,7 +53,7 @@ bot.on("error", (err) => console.error("Bot error:", err.message));
|
|
|
58
53
|
|
|
59
54
|
## Async Iterables
|
|
60
55
|
|
|
61
|
-
|
|
56
|
+
All messages are also available as an `AsyncIterable`, useful for sequential processing with `for await`:
|
|
62
57
|
|
|
63
58
|
```typescript
|
|
64
59
|
import { Bot } from "@scotthamilton77/discord-bot-lib";
|
|
@@ -70,16 +65,14 @@ const bot = await Bot.fromConfig({
|
|
|
70
65
|
});
|
|
71
66
|
|
|
72
67
|
// Process mentions one at a time
|
|
73
|
-
for await (const event of bot.
|
|
68
|
+
for await (const event of bot.messages({ filter: (e) => e.isMention })) {
|
|
74
69
|
await bot.reply(event.channelId, event.messageId, "Got it!");
|
|
75
70
|
}
|
|
76
71
|
```
|
|
77
72
|
|
|
78
73
|
```typescript
|
|
79
|
-
// Filter
|
|
80
|
-
for await (const event of bot.messages("
|
|
81
|
-
filter: (msg) => msg.content.startsWith("!cmd"),
|
|
82
|
-
})) {
|
|
74
|
+
// Filter messages with a predicate
|
|
75
|
+
for await (const event of bot.messages({ filter: (msg) => msg.content.startsWith("!cmd") })) {
|
|
83
76
|
await bot.send(event.channelId, `Command received: ${event.content}`);
|
|
84
77
|
}
|
|
85
78
|
```
|
|
@@ -99,9 +92,11 @@ const botB = await Bot.fromConfig({ id: "b", name: "Bravo", token: TOKEN_B });
|
|
|
99
92
|
manager.addBot(botA);
|
|
100
93
|
manager.addBot(botB);
|
|
101
94
|
|
|
102
|
-
// Single handler receives
|
|
103
|
-
manager.
|
|
104
|
-
|
|
95
|
+
// Single handler receives all messages from all bots
|
|
96
|
+
manager.onMessage((event, bot) => {
|
|
97
|
+
if (event.isMention) {
|
|
98
|
+
console.log(`${bot.name} was mentioned by ${event.author.username}`);
|
|
99
|
+
}
|
|
105
100
|
});
|
|
106
101
|
|
|
107
102
|
// Lifecycle events
|
|
@@ -266,12 +261,12 @@ const { buffer, filename, contentType } = await downloadAttachment(attachment);
|
|
|
266
261
|
|---|---|
|
|
267
262
|
| `BotConfig` | Configuration for `Bot.fromConfig()` -- id, name, token, optional intents/channels/cacheSize. |
|
|
268
263
|
| `BotStatus` | `"unregistered" \| "configuring" \| "connecting" \| "verifying" \| "ready" \| "disconnected" \| "failed"` |
|
|
269
|
-
| `MessageEvent` | Incoming message with author, content, channelId, mentions, and `raw` discord.js Message. |
|
|
264
|
+
| `MessageEvent` | Incoming message with routing metadata (`isDM`, `isMention`, `isReply`, `replyTo`), author, content, channelId, mentions, and `raw` discord.js Message. |
|
|
270
265
|
| `SentMessage` | Tracked outgoing message with messageId, channelId, timestamp, and `raw` escape hatch. |
|
|
271
266
|
| `MessageContent` | `string \| { content?: string; embeds?: unknown[]; files?: FileAttachment[] }` |
|
|
272
267
|
| `FetchedMessage` | Lightweight message from `fetchMessages()` / `fetchHistory()`. |
|
|
273
268
|
| `FetchHistoryOptions` | Discriminated union: `{ after: string } \| { before: string }` with optional `limit`. |
|
|
274
|
-
| `MessageFilter` | `(message: MessageEvent) => boolean` predicate for `
|
|
269
|
+
| `MessageFilter` | `(message: MessageEvent) => boolean` predicate for `messages({ filter })`. |
|
|
275
270
|
| `FileAttachment` | `{ data: Buffer \| string; name: string }` for sending files. |
|
|
276
271
|
| `OnboardingStep` | Step in the onboarding flow with id, label, instructions, status, and `complete()`. |
|
|
277
272
|
| `AttachmentLike` | Minimal shape for attachment validation utilities. |
|
package/dist/index.cjs
CHANGED
|
@@ -43,6 +43,10 @@ var EventBuffer = class {
|
|
|
43
43
|
buffer = [];
|
|
44
44
|
resolve = null;
|
|
45
45
|
closed = false;
|
|
46
|
+
onClose;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.onClose = options?.onClose;
|
|
49
|
+
}
|
|
46
50
|
push(value) {
|
|
47
51
|
if (this.closed) return;
|
|
48
52
|
if (this.resolve) {
|
|
@@ -54,7 +58,9 @@ var EventBuffer = class {
|
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
close() {
|
|
61
|
+
if (this.closed) return;
|
|
57
62
|
this.closed = true;
|
|
63
|
+
this.onClose?.();
|
|
58
64
|
if (this.resolve) {
|
|
59
65
|
const r = this.resolve;
|
|
60
66
|
this.resolve = null;
|
|
@@ -77,6 +83,10 @@ var EventBuffer = class {
|
|
|
77
83
|
return new Promise((resolve) => {
|
|
78
84
|
this.resolve = resolve;
|
|
79
85
|
});
|
|
86
|
+
},
|
|
87
|
+
return: () => {
|
|
88
|
+
this.close();
|
|
89
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
80
90
|
}
|
|
81
91
|
};
|
|
82
92
|
}
|
|
@@ -153,9 +163,7 @@ var Bot = class _Bot {
|
|
|
153
163
|
client;
|
|
154
164
|
sentMessages;
|
|
155
165
|
_connectedAt = null;
|
|
156
|
-
|
|
157
|
-
replyHandlers = [];
|
|
158
|
-
channelSubscriptions = /* @__PURE__ */ new Map();
|
|
166
|
+
messageHandlers = [];
|
|
159
167
|
errorHandlers = [];
|
|
160
168
|
_includeBotMessages;
|
|
161
169
|
constructor(id, name, client, sentMessageCacheSize, includeBotMessages) {
|
|
@@ -221,18 +229,10 @@ var Bot = class _Bot {
|
|
|
221
229
|
);
|
|
222
230
|
}
|
|
223
231
|
async reply(channelId, messageId, content) {
|
|
224
|
-
const channel = await this.
|
|
232
|
+
const { channel, message } = await this.fetchMessage(channelId, messageId, "reply to");
|
|
225
233
|
if (!("send" in channel)) {
|
|
226
234
|
throw new Error(`Channel ${channelId} is not a sendable channel`);
|
|
227
235
|
}
|
|
228
|
-
let message;
|
|
229
|
-
try {
|
|
230
|
-
message = await channel.messages.fetch(messageId);
|
|
231
|
-
} catch (error) {
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Cannot reply to message ${messageId}: ${error instanceof Error ? error.message : String(error)}`
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
236
|
return this.sendPayloads(content, (payload, i) => {
|
|
237
237
|
if (i === 0) {
|
|
238
238
|
return message.reply(payload);
|
|
@@ -242,9 +242,8 @@ var Bot = class _Bot {
|
|
|
242
242
|
}
|
|
243
243
|
// --- Channel operations ---
|
|
244
244
|
async react(channelId, messageId, emoji) {
|
|
245
|
-
const
|
|
245
|
+
const { message } = await this.fetchMessage(channelId, messageId, "react to");
|
|
246
246
|
try {
|
|
247
|
-
const message = await channel.messages.fetch(messageId);
|
|
248
247
|
await message.react(emoji);
|
|
249
248
|
} catch (error) {
|
|
250
249
|
throw new Error(
|
|
@@ -253,9 +252,8 @@ var Bot = class _Bot {
|
|
|
253
252
|
}
|
|
254
253
|
}
|
|
255
254
|
async editMessage(channelId, messageId, content) {
|
|
256
|
-
const
|
|
255
|
+
const { message } = await this.fetchMessage(channelId, messageId, "edit");
|
|
257
256
|
try {
|
|
258
|
-
const message = await channel.messages.fetch(messageId);
|
|
259
257
|
await message.edit(content);
|
|
260
258
|
} catch (error) {
|
|
261
259
|
throw new Error(
|
|
@@ -306,76 +304,48 @@ var Bot = class _Bot {
|
|
|
306
304
|
}
|
|
307
305
|
}
|
|
308
306
|
async getMessageAttachments(channelId, messageId) {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
contentType: att.contentType
|
|
318
|
-
}));
|
|
319
|
-
} catch (error) {
|
|
320
|
-
throw new Error(
|
|
321
|
-
`Cannot fetch attachments for message ${messageId}: ${error instanceof Error ? error.message : String(error)}`
|
|
322
|
-
);
|
|
323
|
-
}
|
|
307
|
+
const { message } = await this.fetchMessage(channelId, messageId, "fetch attachments for");
|
|
308
|
+
return [...message.attachments.values()].map((att) => ({
|
|
309
|
+
id: att.id,
|
|
310
|
+
size: att.size,
|
|
311
|
+
name: att.name,
|
|
312
|
+
url: att.url,
|
|
313
|
+
contentType: att.contentType
|
|
314
|
+
}));
|
|
324
315
|
}
|
|
325
316
|
// --- Receiving (event-driven) ---
|
|
326
|
-
|
|
327
|
-
this.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (typeof handlerOrOpts === "function") {
|
|
336
|
-
handler = handlerOrOpts;
|
|
337
|
-
} else {
|
|
338
|
-
filter = handlerOrOpts.filter;
|
|
339
|
-
if (!maybeHandler) {
|
|
340
|
-
throw new Error("Handler is required when providing filter options");
|
|
341
|
-
}
|
|
342
|
-
handler = maybeHandler;
|
|
343
|
-
}
|
|
344
|
-
const subs = this.channelSubscriptions.get(channelId) ?? [];
|
|
345
|
-
subs.push({ filter, handler });
|
|
346
|
-
this.channelSubscriptions.set(channelId, subs);
|
|
317
|
+
onMessage(handler) {
|
|
318
|
+
this.messageHandlers.push(handler);
|
|
319
|
+
let removed = false;
|
|
320
|
+
return () => {
|
|
321
|
+
if (removed) return;
|
|
322
|
+
removed = true;
|
|
323
|
+
const index = this.messageHandlers.indexOf(handler);
|
|
324
|
+
if (index !== -1) this.messageHandlers.splice(index, 1);
|
|
325
|
+
};
|
|
347
326
|
}
|
|
348
327
|
// --- Receiving (async iterable) ---
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
328
|
+
messages(options) {
|
|
329
|
+
let unsubscribe;
|
|
330
|
+
const buffer = new EventBuffer({
|
|
331
|
+
onClose: () => unsubscribe?.()
|
|
353
332
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const buffer = new EventBuffer();
|
|
358
|
-
this.onReply((event, original) => {
|
|
359
|
-
buffer.push([event, original]);
|
|
333
|
+
unsubscribe = this.onMessage((event) => {
|
|
334
|
+
if (options?.filter && !options.filter(event)) return;
|
|
335
|
+
buffer.push(event);
|
|
360
336
|
});
|
|
361
337
|
return buffer;
|
|
362
338
|
}
|
|
363
|
-
messages(channelId, options) {
|
|
364
|
-
const buffer = new EventBuffer();
|
|
365
|
-
if (options?.filter) {
|
|
366
|
-
this.onMessage(channelId, { filter: options.filter }, (event) => {
|
|
367
|
-
buffer.push(event);
|
|
368
|
-
});
|
|
369
|
-
} else {
|
|
370
|
-
this.onMessage(channelId, (event) => {
|
|
371
|
-
buffer.push(event);
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
return buffer;
|
|
375
|
-
}
|
|
376
339
|
// --- Error handling ---
|
|
377
340
|
on(_event, handler) {
|
|
378
341
|
this.errorHandlers.push(handler);
|
|
342
|
+
let removed = false;
|
|
343
|
+
return () => {
|
|
344
|
+
if (removed) return;
|
|
345
|
+
removed = true;
|
|
346
|
+
const index = this.errorHandlers.indexOf(handler);
|
|
347
|
+
if (index !== -1) this.errorHandlers.splice(index, 1);
|
|
348
|
+
};
|
|
379
349
|
}
|
|
380
350
|
emitError(error) {
|
|
381
351
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -387,6 +357,8 @@ var Bot = class _Bot {
|
|
|
387
357
|
async disconnect() {
|
|
388
358
|
this.client.removeAllListeners();
|
|
389
359
|
await this.client.destroy();
|
|
360
|
+
this.messageHandlers.length = 0;
|
|
361
|
+
this.errorHandlers.length = 0;
|
|
390
362
|
this._status = "disconnected";
|
|
391
363
|
this._connectedAt = null;
|
|
392
364
|
}
|
|
@@ -400,6 +372,17 @@ var Bot = class _Bot {
|
|
|
400
372
|
}
|
|
401
373
|
return channel;
|
|
402
374
|
}
|
|
375
|
+
async fetchMessage(channelId, messageId, verb) {
|
|
376
|
+
const channel = await this.fetchTextChannel(channelId);
|
|
377
|
+
try {
|
|
378
|
+
const message = await channel.messages.fetch(messageId);
|
|
379
|
+
return { channel, message };
|
|
380
|
+
} catch (error) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Cannot ${verb} message ${messageId}: ${error instanceof Error ? error.message : String(error)}`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
403
386
|
toFetchedMessage(msg) {
|
|
404
387
|
return {
|
|
405
388
|
messageId: msg.id,
|
|
@@ -447,27 +430,8 @@ var Bot = class _Bot {
|
|
|
447
430
|
if (message.author.id === this.client.user?.id) return;
|
|
448
431
|
if (message.author.bot && !this._includeBotMessages) return;
|
|
449
432
|
const event = this.toMessageEvent(message);
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
if (botUser && (message.mentions.has(botUser.id) || isDM)) {
|
|
453
|
-
for (const handler of this.mentionHandlers) {
|
|
454
|
-
handler(event);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
if (message.reference?.messageId) {
|
|
458
|
-
const original = this.sentMessages.get(message.reference.messageId);
|
|
459
|
-
if (original) {
|
|
460
|
-
for (const handler of this.replyHandlers) {
|
|
461
|
-
handler(event, original);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
const subs = this.channelSubscriptions.get(message.channelId);
|
|
466
|
-
if (subs) {
|
|
467
|
-
for (const sub of subs) {
|
|
468
|
-
if (sub.filter && !sub.filter(event)) continue;
|
|
469
|
-
sub.handler(event);
|
|
470
|
-
}
|
|
433
|
+
for (const handler of this.messageHandlers) {
|
|
434
|
+
handler(event);
|
|
471
435
|
}
|
|
472
436
|
} catch (error) {
|
|
473
437
|
this.emitError(error);
|
|
@@ -475,6 +439,18 @@ var Bot = class _Bot {
|
|
|
475
439
|
});
|
|
476
440
|
}
|
|
477
441
|
toMessageEvent(message) {
|
|
442
|
+
const isDM = message.channel.type === import_discord.ChannelType.DM;
|
|
443
|
+
const botUser = this.client.user;
|
|
444
|
+
const isMention = isDM || (botUser ? message.mentions.has(botUser.id) : false);
|
|
445
|
+
let isReply = false;
|
|
446
|
+
let replyTo = null;
|
|
447
|
+
if (message.reference?.messageId) {
|
|
448
|
+
const original = this.sentMessages.get(message.reference.messageId);
|
|
449
|
+
if (original) {
|
|
450
|
+
isReply = true;
|
|
451
|
+
replyTo = original;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
478
454
|
return {
|
|
479
455
|
messageId: message.id,
|
|
480
456
|
author: {
|
|
@@ -487,7 +463,11 @@ var Bot = class _Bot {
|
|
|
487
463
|
guildId: message.guildId ?? null,
|
|
488
464
|
timestamp: message.createdAt,
|
|
489
465
|
mentions: [...message.mentions.users.keys()],
|
|
490
|
-
raw: message
|
|
466
|
+
raw: message,
|
|
467
|
+
isDM,
|
|
468
|
+
isMention,
|
|
469
|
+
isReply,
|
|
470
|
+
replyTo
|
|
491
471
|
};
|
|
492
472
|
}
|
|
493
473
|
async sendPayloads(content, dispatcher) {
|
|
@@ -694,21 +674,28 @@ var BotOnboarding = class {
|
|
|
694
674
|
// src/connector-manager.ts
|
|
695
675
|
var ConnectorManager = class {
|
|
696
676
|
botMap = /* @__PURE__ */ new Map();
|
|
697
|
-
|
|
677
|
+
botUnsubscribers = /* @__PURE__ */ new Map();
|
|
678
|
+
messageHandlers = [];
|
|
698
679
|
lifecycleHandlers = /* @__PURE__ */ new Map();
|
|
699
680
|
addBot(bot) {
|
|
700
681
|
if (this.botMap.has(bot.id)) {
|
|
701
682
|
throw new Error(`Bot with id "${bot.id}" already exists in this manager.`);
|
|
702
683
|
}
|
|
703
684
|
this.botMap.set(bot.id, bot);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
685
|
+
const unsubs = [];
|
|
686
|
+
unsubs.push(
|
|
687
|
+
bot.onMessage((event) => {
|
|
688
|
+
for (const handler of this.messageHandlers) {
|
|
689
|
+
handler(event, bot);
|
|
690
|
+
}
|
|
691
|
+
})
|
|
692
|
+
);
|
|
693
|
+
unsubs.push(
|
|
694
|
+
bot.on("error", (error) => {
|
|
695
|
+
this.emitLifecycle("botError", bot, error);
|
|
696
|
+
})
|
|
697
|
+
);
|
|
698
|
+
this.botUnsubscribers.set(bot.id, unsubs);
|
|
712
699
|
}
|
|
713
700
|
getBot(id) {
|
|
714
701
|
return this.botMap.get(id);
|
|
@@ -717,6 +704,11 @@ var ConnectorManager = class {
|
|
|
717
704
|
return this.botMap.values();
|
|
718
705
|
}
|
|
719
706
|
removeBot(id) {
|
|
707
|
+
const unsubs = this.botUnsubscribers.get(id);
|
|
708
|
+
if (unsubs) {
|
|
709
|
+
for (const unsub of unsubs) unsub();
|
|
710
|
+
this.botUnsubscribers.delete(id);
|
|
711
|
+
}
|
|
720
712
|
this.botMap.delete(id);
|
|
721
713
|
}
|
|
722
714
|
status() {
|
|
@@ -726,8 +718,8 @@ var ConnectorManager = class {
|
|
|
726
718
|
status: bot.status
|
|
727
719
|
}));
|
|
728
720
|
}
|
|
729
|
-
|
|
730
|
-
this.
|
|
721
|
+
onMessage(handler) {
|
|
722
|
+
this.messageHandlers.push(handler);
|
|
731
723
|
}
|
|
732
724
|
on(event, callback) {
|
|
733
725
|
const handlers = this.lifecycleHandlers.get(event) ?? [];
|