@scotthamilton77/discord-bot-lib 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +287 -0
- package/dist/index.cjs +766 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +258 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +729 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Scott Hamilton
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# @scotthamilton77/discord-bot-lib
|
|
2
|
+
|
|
3
|
+
<!-- Badges: npm version, CI status, license, Node version — add when publishing -->
|
|
4
|
+
|
|
5
|
+
TypeScript library for managing Discord bot identities, messaging, and multi-bot coordination. Wraps discord.js with a clean API surface: factory-based construction, event-driven callbacks, async iterables, and built-in message chunking.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Factory construction** -- `Bot.fromConfig()` connects, verifies guilds, and returns a ready bot in one call
|
|
10
|
+
- **Dual receive API** -- event-driven callbacks (`onMention`, `onReply`, `onMessage`) and async iterables (`mentions()`, `replies()`, `messages()`)
|
|
11
|
+
- **Multi-bot management** -- `ConnectorManager` aggregates mentions across bots with lifecycle events
|
|
12
|
+
- **Guided onboarding** -- `BotOnboarding` walks through token validation, server invitation, and permission verification
|
|
13
|
+
- **Message chunking** -- automatically splits messages at paragraph/line/word boundaries to stay within Discord's 2000-char limit
|
|
14
|
+
- **File attachments** -- send files from Buffer or file path, with size validation and filename sanitization
|
|
15
|
+
- **History pagination** -- `fetchHistory()` async generator pages through channel history with automatic rate-limit retry
|
|
16
|
+
- **Dual build** -- ships ESM + CJS + type declarations via tsup
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @scotthamilton77/discord-bot-lib
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
discord.js is a direct dependency -- no separate peer install needed.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Bot } from "@scotthamilton77/discord-bot-lib";
|
|
30
|
+
|
|
31
|
+
const bot = await Bot.fromConfig({
|
|
32
|
+
id: "my-bot",
|
|
33
|
+
name: "GreeterBot",
|
|
34
|
+
token: process.env.DISCORD_TOKEN!,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Respond to @mentions
|
|
38
|
+
bot.onMention(async (event) => {
|
|
39
|
+
await bot.send(event.channelId, `Hello, ${event.author.username}!`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Respond to replies to messages this bot sent
|
|
43
|
+
bot.onReply(async (event, original) => {
|
|
44
|
+
await bot.send(event.channelId, `You replied to my message ${original.messageId}`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Listen to all messages in a specific channel
|
|
48
|
+
bot.onMessage("123456789012345678", (event) => {
|
|
49
|
+
console.log(`${event.author.username}: ${event.content}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Error handling
|
|
53
|
+
bot.on("error", (err) => console.error("Bot error:", err.message));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Async Iterables
|
|
57
|
+
|
|
58
|
+
Every event type is also available as an `AsyncIterable`, useful for sequential processing with `for await`:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { Bot } from "@scotthamilton77/discord-bot-lib";
|
|
62
|
+
|
|
63
|
+
const bot = await Bot.fromConfig({
|
|
64
|
+
id: "iter-bot",
|
|
65
|
+
name: "IterBot",
|
|
66
|
+
token: process.env.DISCORD_TOKEN!,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Process mentions one at a time
|
|
70
|
+
for await (const event of bot.mentions()) {
|
|
71
|
+
await bot.reply(event.channelId, event.messageId, "Got it!");
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Filter channel messages with a predicate
|
|
77
|
+
for await (const event of bot.messages("123456789012345678", {
|
|
78
|
+
filter: (msg) => msg.content.startsWith("!cmd"),
|
|
79
|
+
})) {
|
|
80
|
+
await bot.send(event.channelId, `Command received: ${event.content}`);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Multi-Bot Management
|
|
85
|
+
|
|
86
|
+
`ConnectorManager` registers multiple bots and aggregates their events:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { Bot, ConnectorManager } from "@scotthamilton77/discord-bot-lib";
|
|
90
|
+
|
|
91
|
+
const manager = new ConnectorManager();
|
|
92
|
+
|
|
93
|
+
const botA = await Bot.fromConfig({ id: "a", name: "Alpha", token: TOKEN_A });
|
|
94
|
+
const botB = await Bot.fromConfig({ id: "b", name: "Bravo", token: TOKEN_B });
|
|
95
|
+
|
|
96
|
+
manager.addBot(botA);
|
|
97
|
+
manager.addBot(botB);
|
|
98
|
+
|
|
99
|
+
// Single handler receives mentions from all bots
|
|
100
|
+
manager.onMention((event, bot) => {
|
|
101
|
+
console.log(`${bot.name} was mentioned by ${event.author.username}`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Lifecycle events
|
|
105
|
+
manager.on("botError", (bot, error) => {
|
|
106
|
+
console.error(`Error from ${(bot as { name: string }).name}:`, error);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Status overview
|
|
110
|
+
console.log(manager.status());
|
|
111
|
+
// [{ id: "a", name: "Alpha", status: "ready" }, ...]
|
|
112
|
+
|
|
113
|
+
// Clean shutdown
|
|
114
|
+
await manager.shutdown();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Bot Onboarding
|
|
118
|
+
|
|
119
|
+
`BotOnboarding` provides a guided 3-step setup flow -- useful for interactive CLIs or admin panels:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { BotOnboarding } from "@scotthamilton77/discord-bot-lib";
|
|
123
|
+
|
|
124
|
+
const onboarding = new BotOnboarding("new-bot", "NewBot");
|
|
125
|
+
|
|
126
|
+
// Steps: provide_token -> invite_to_server -> verify_permissions
|
|
127
|
+
for (const step of onboarding.steps) {
|
|
128
|
+
console.log(`${step.id}: ${step.label} [${step.status}]`);
|
|
129
|
+
console.log(` ${step.instructions}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Complete steps in order
|
|
133
|
+
const tokenResult = await onboarding.steps[0]!.complete(process.env.DISCORD_TOKEN!);
|
|
134
|
+
if (!tokenResult.success) throw new Error(tokenResult.error);
|
|
135
|
+
|
|
136
|
+
const inviteResult = await onboarding.steps[1]!.complete();
|
|
137
|
+
if (!inviteResult.success) throw new Error(inviteResult.error);
|
|
138
|
+
|
|
139
|
+
const verifyResult = await onboarding.steps[2]!.complete();
|
|
140
|
+
if (!verifyResult.success) throw new Error(verifyResult.error);
|
|
141
|
+
|
|
142
|
+
// Ready bot is available after successful onboarding
|
|
143
|
+
const bot = onboarding.bot!;
|
|
144
|
+
console.log(`${bot.name} is ${bot.status}`);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Sending Messages
|
|
148
|
+
|
|
149
|
+
Messages can be plain strings, or objects with optional embeds and file attachments:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Plain text (auto-chunked if over 2000 chars)
|
|
153
|
+
await bot.send(channelId, "Hello, world!");
|
|
154
|
+
|
|
155
|
+
// With embeds
|
|
156
|
+
await bot.send(channelId, {
|
|
157
|
+
content: "Check this out:",
|
|
158
|
+
embeds: [{ title: "My Embed", description: "Some rich content" }],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// With file attachments (Buffer or absolute file path)
|
|
162
|
+
await bot.send(channelId, {
|
|
163
|
+
content: "Here's the report:",
|
|
164
|
+
files: [
|
|
165
|
+
{ name: "report.csv", data: Buffer.from("a,b,c\n1,2,3") },
|
|
166
|
+
{ name: "chart.png", data: "/absolute/path/to/chart.png" },
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Reply to a specific message
|
|
171
|
+
await bot.reply(channelId, messageId, "Thanks for your message!");
|
|
172
|
+
|
|
173
|
+
// Direct message
|
|
174
|
+
await bot.sendDM(userId, "Private hello!");
|
|
175
|
+
|
|
176
|
+
// Reactions
|
|
177
|
+
await bot.react(channelId, messageId, "\ud83d\udc4d");
|
|
178
|
+
|
|
179
|
+
// Edit a message
|
|
180
|
+
await bot.editMessage(channelId, messageId, "Updated content");
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Fetching History
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// Fetch recent messages (up to 100)
|
|
187
|
+
const recent = await bot.fetchMessages(channelId, 50);
|
|
188
|
+
|
|
189
|
+
// Page through history with an async generator
|
|
190
|
+
for await (const page of bot.fetchHistory(channelId, { after: lastSeenId, limit: 1000 })) {
|
|
191
|
+
for (const msg of page) {
|
|
192
|
+
console.log(`${msg.author.username}: ${msg.content}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Fetch backwards from a message
|
|
197
|
+
for await (const page of bot.fetchHistory(channelId, { before: messageId, limit: 500 })) {
|
|
198
|
+
// pages arrive in reverse chronological order
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Get attachments for a specific message
|
|
202
|
+
const attachments = await bot.getMessageAttachments(channelId, messageId);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Message Utilities
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import {
|
|
209
|
+
chunkMessage,
|
|
210
|
+
DISCORD_MAX_MESSAGE_LENGTH,
|
|
211
|
+
validateAttachmentSize,
|
|
212
|
+
sanitizeAttachmentName,
|
|
213
|
+
downloadAttachment,
|
|
214
|
+
MAX_ATTACHMENT_BYTES,
|
|
215
|
+
} from "@scotthamilton77/discord-bot-lib";
|
|
216
|
+
|
|
217
|
+
// Split long text into Discord-safe chunks
|
|
218
|
+
const chunks = chunkMessage(longText);
|
|
219
|
+
// Splits at paragraph -> line -> word -> hard-cut boundaries
|
|
220
|
+
|
|
221
|
+
// Validate attachment size (throws if > 25 MB)
|
|
222
|
+
validateAttachmentSize({ size: file.size, name: file.name, id: file.id });
|
|
223
|
+
|
|
224
|
+
// Sanitize filenames (replaces unsafe chars with underscores)
|
|
225
|
+
const safeName = sanitizeAttachmentName({ name: "report[final].csv", id: "123" });
|
|
226
|
+
// "report_final_.csv"
|
|
227
|
+
|
|
228
|
+
// Download a Discord attachment to a buffer
|
|
229
|
+
const { buffer, filename, contentType } = await downloadAttachment(attachment);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## API Reference
|
|
233
|
+
|
|
234
|
+
### Classes
|
|
235
|
+
|
|
236
|
+
| Export | Description |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `Bot` | Single bot identity. Created via `Bot.fromConfig(config)`. Handles sending, receiving, and channel operations. |
|
|
239
|
+
| `ConnectorManager` | Multi-bot registry with aggregated mention handling and lifecycle events. |
|
|
240
|
+
| `BotOnboarding` | Guided 3-step setup: token validation, server invite, permission check. |
|
|
241
|
+
| `EventBuffer<T>` | Single-consumer `AsyncIterable<T>` bridge between push events and `for await` loops. |
|
|
242
|
+
|
|
243
|
+
### Functions
|
|
244
|
+
|
|
245
|
+
| Export | Description |
|
|
246
|
+
|---|---|
|
|
247
|
+
| `chunkMessage(text, limit?)` | Split text into chunks within Discord's 2000-char limit. |
|
|
248
|
+
| `validateAttachmentSize(attachment)` | Throws if attachment exceeds 25 MB. |
|
|
249
|
+
| `sanitizeAttachmentName(attachment)` | Replace unsafe filename characters with underscores. |
|
|
250
|
+
| `downloadAttachment(attachment)` | Download a Discord attachment to a `Buffer`. |
|
|
251
|
+
|
|
252
|
+
### Constants
|
|
253
|
+
|
|
254
|
+
| Export | Value |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `DISCORD_MAX_MESSAGE_LENGTH` | `2000` |
|
|
257
|
+
| `MAX_ATTACHMENT_BYTES` | `26214400` (25 MB) |
|
|
258
|
+
| `DEFAULT_SENT_MESSAGE_CACHE_SIZE` | `1000` |
|
|
259
|
+
|
|
260
|
+
### Key Types
|
|
261
|
+
|
|
262
|
+
| Type | Description |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `BotConfig` | Configuration for `Bot.fromConfig()` -- id, name, token, optional intents/channels/cacheSize. |
|
|
265
|
+
| `BotStatus` | `"unregistered" \| "configuring" \| "connecting" \| "verifying" \| "ready" \| "disconnected" \| "failed"` |
|
|
266
|
+
| `MessageEvent` | Incoming message with author, content, channelId, mentions, and `raw` discord.js Message. |
|
|
267
|
+
| `SentMessage` | Tracked outgoing message with messageId, channelId, timestamp, and `raw` escape hatch. |
|
|
268
|
+
| `MessageContent` | `string \| { content?: string; embeds?: unknown[]; files?: FileAttachment[] }` |
|
|
269
|
+
| `FetchedMessage` | Lightweight message from `fetchMessages()` / `fetchHistory()`. |
|
|
270
|
+
| `FetchHistoryOptions` | Discriminated union: `{ after: string } \| { before: string }` with optional `limit`. |
|
|
271
|
+
| `MessageFilter` | `(message: MessageEvent) => boolean` predicate for `onMessage()` / `messages()`. |
|
|
272
|
+
| `FileAttachment` | `{ data: Buffer \| string; name: string }` for sending files. |
|
|
273
|
+
| `OnboardingStep` | Step in the onboarding flow with id, label, instructions, status, and `complete()`. |
|
|
274
|
+
| `AttachmentLike` | Minimal shape for attachment validation utilities. |
|
|
275
|
+
| `DownloadableAttachment` | Extends `AttachmentLike` with `url` and `contentType` for downloading. |
|
|
276
|
+
| `DownloadedAttachment` | Result of `downloadAttachment()`: buffer, filename, contentType. |
|
|
277
|
+
| `ManagerEvents` | Lifecycle event signatures for `ConnectorManager`. |
|
|
278
|
+
|
|
279
|
+
## Requirements
|
|
280
|
+
|
|
281
|
+
- Node.js >= 20
|
|
282
|
+
- TypeScript >= 5.5 (for development)
|
|
283
|
+
- discord.js ^14.25 (included as a dependency)
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
ISC
|