@scotthamilton77/discord-bot-lib
v0.1.0
Published
TypeScript library for managing Discord bot identities, messaging, and multi-bot coordination
Maintainers
Readme
@scotthamilton77/discord-bot-lib
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.
Features
- Factory construction --
Bot.fromConfig()connects, verifies guilds, and returns a ready bot in one call - Dual receive API -- event-driven callbacks (
onMention,onReply,onMessage) and async iterables (mentions(),replies(),messages()) - Multi-bot management --
ConnectorManageraggregates mentions across bots with lifecycle events - Guided onboarding --
BotOnboardingwalks through token validation, server invitation, and permission verification - Message chunking -- automatically splits messages at paragraph/line/word boundaries to stay within Discord's 2000-char limit
- File attachments -- send files from Buffer or file path, with size validation and filename sanitization
- History pagination --
fetchHistory()async generator pages through channel history with automatic rate-limit retry - Dual build -- ships ESM + CJS + type declarations via tsup
Installation
npm install @scotthamilton77/discord-bot-libdiscord.js is a direct dependency -- no separate peer install needed.
Quick Start
import { Bot } from "@scotthamilton77/discord-bot-lib";
const bot = await Bot.fromConfig({
id: "my-bot",
name: "GreeterBot",
token: process.env.DISCORD_TOKEN!,
});
// Respond to @mentions
bot.onMention(async (event) => {
await bot.send(event.channelId, `Hello, ${event.author.username}!`);
});
// Respond to replies to messages this bot sent
bot.onReply(async (event, original) => {
await bot.send(event.channelId, `You replied to my message ${original.messageId}`);
});
// Listen to all messages in a specific channel
bot.onMessage("123456789012345678", (event) => {
console.log(`${event.author.username}: ${event.content}`);
});
// Error handling
bot.on("error", (err) => console.error("Bot error:", err.message));Async Iterables
Every event type is also available as an AsyncIterable, useful for sequential processing with for await:
import { Bot } from "@scotthamilton77/discord-bot-lib";
const bot = await Bot.fromConfig({
id: "iter-bot",
name: "IterBot",
token: process.env.DISCORD_TOKEN!,
});
// Process mentions one at a time
for await (const event of bot.mentions()) {
await bot.reply(event.channelId, event.messageId, "Got it!");
}// Filter channel messages with a predicate
for await (const event of bot.messages("123456789012345678", {
filter: (msg) => msg.content.startsWith("!cmd"),
})) {
await bot.send(event.channelId, `Command received: ${event.content}`);
}Multi-Bot Management
ConnectorManager registers multiple bots and aggregates their events:
import { Bot, ConnectorManager } from "@scotthamilton77/discord-bot-lib";
const manager = new ConnectorManager();
const botA = await Bot.fromConfig({ id: "a", name: "Alpha", token: TOKEN_A });
const botB = await Bot.fromConfig({ id: "b", name: "Bravo", token: TOKEN_B });
manager.addBot(botA);
manager.addBot(botB);
// Single handler receives mentions from all bots
manager.onMention((event, bot) => {
console.log(`${bot.name} was mentioned by ${event.author.username}`);
});
// Lifecycle events
manager.on("botError", (bot, error) => {
console.error(`Error from ${(bot as { name: string }).name}:`, error);
});
// Status overview
console.log(manager.status());
// [{ id: "a", name: "Alpha", status: "ready" }, ...]
// Clean shutdown
await manager.shutdown();Bot Onboarding
BotOnboarding provides a guided 3-step setup flow -- useful for interactive CLIs or admin panels:
import { BotOnboarding } from "@scotthamilton77/discord-bot-lib";
const onboarding = new BotOnboarding("new-bot", "NewBot");
// Steps: provide_token -> invite_to_server -> verify_permissions
for (const step of onboarding.steps) {
console.log(`${step.id}: ${step.label} [${step.status}]`);
console.log(` ${step.instructions}`);
}
// Complete steps in order
const tokenResult = await onboarding.steps[0]!.complete(process.env.DISCORD_TOKEN!);
if (!tokenResult.success) throw new Error(tokenResult.error);
const inviteResult = await onboarding.steps[1]!.complete();
if (!inviteResult.success) throw new Error(inviteResult.error);
const verifyResult = await onboarding.steps[2]!.complete();
if (!verifyResult.success) throw new Error(verifyResult.error);
// Ready bot is available after successful onboarding
const bot = onboarding.bot!;
console.log(`${bot.name} is ${bot.status}`);Sending Messages
Messages can be plain strings, or objects with optional embeds and file attachments:
// Plain text (auto-chunked if over 2000 chars)
await bot.send(channelId, "Hello, world!");
// With embeds
await bot.send(channelId, {
content: "Check this out:",
embeds: [{ title: "My Embed", description: "Some rich content" }],
});
// With file attachments (Buffer or absolute file path)
await bot.send(channelId, {
content: "Here's the report:",
files: [
{ name: "report.csv", data: Buffer.from("a,b,c\n1,2,3") },
{ name: "chart.png", data: "/absolute/path/to/chart.png" },
],
});
// Reply to a specific message
await bot.reply(channelId, messageId, "Thanks for your message!");
// Direct message
await bot.sendDM(userId, "Private hello!");
// Reactions
await bot.react(channelId, messageId, "\ud83d\udc4d");
// Edit a message
await bot.editMessage(channelId, messageId, "Updated content");Fetching History
// Fetch recent messages (up to 100)
const recent = await bot.fetchMessages(channelId, 50);
// Page through history with an async generator
for await (const page of bot.fetchHistory(channelId, { after: lastSeenId, limit: 1000 })) {
for (const msg of page) {
console.log(`${msg.author.username}: ${msg.content}`);
}
}
// Fetch backwards from a message
for await (const page of bot.fetchHistory(channelId, { before: messageId, limit: 500 })) {
// pages arrive in reverse chronological order
}
// Get attachments for a specific message
const attachments = await bot.getMessageAttachments(channelId, messageId);Message Utilities
import {
chunkMessage,
DISCORD_MAX_MESSAGE_LENGTH,
validateAttachmentSize,
sanitizeAttachmentName,
downloadAttachment,
MAX_ATTACHMENT_BYTES,
} from "@scotthamilton77/discord-bot-lib";
// Split long text into Discord-safe chunks
const chunks = chunkMessage(longText);
// Splits at paragraph -> line -> word -> hard-cut boundaries
// Validate attachment size (throws if > 25 MB)
validateAttachmentSize({ size: file.size, name: file.name, id: file.id });
// Sanitize filenames (replaces unsafe chars with underscores)
const safeName = sanitizeAttachmentName({ name: "report[final].csv", id: "123" });
// "report_final_.csv"
// Download a Discord attachment to a buffer
const { buffer, filename, contentType } = await downloadAttachment(attachment);API Reference
Classes
| Export | Description |
|---|---|
| Bot | Single bot identity. Created via Bot.fromConfig(config). Handles sending, receiving, and channel operations. |
| ConnectorManager | Multi-bot registry with aggregated mention handling and lifecycle events. |
| BotOnboarding | Guided 3-step setup: token validation, server invite, permission check. |
| EventBuffer<T> | Single-consumer AsyncIterable<T> bridge between push events and for await loops. |
Functions
| Export | Description |
|---|---|
| chunkMessage(text, limit?) | Split text into chunks within Discord's 2000-char limit. |
| validateAttachmentSize(attachment) | Throws if attachment exceeds 25 MB. |
| sanitizeAttachmentName(attachment) | Replace unsafe filename characters with underscores. |
| downloadAttachment(attachment) | Download a Discord attachment to a Buffer. |
Constants
| Export | Value |
|---|---|
| DISCORD_MAX_MESSAGE_LENGTH | 2000 |
| MAX_ATTACHMENT_BYTES | 26214400 (25 MB) |
| DEFAULT_SENT_MESSAGE_CACHE_SIZE | 1000 |
Key Types
| Type | Description |
|---|---|
| BotConfig | Configuration for Bot.fromConfig() -- id, name, token, optional intents/channels/cacheSize. |
| BotStatus | "unregistered" \| "configuring" \| "connecting" \| "verifying" \| "ready" \| "disconnected" \| "failed" |
| MessageEvent | Incoming message with author, content, channelId, mentions, and raw discord.js Message. |
| SentMessage | Tracked outgoing message with messageId, channelId, timestamp, and raw escape hatch. |
| MessageContent | string \| { content?: string; embeds?: unknown[]; files?: FileAttachment[] } |
| FetchedMessage | Lightweight message from fetchMessages() / fetchHistory(). |
| FetchHistoryOptions | Discriminated union: { after: string } \| { before: string } with optional limit. |
| MessageFilter | (message: MessageEvent) => boolean predicate for onMessage() / messages(). |
| FileAttachment | { data: Buffer \| string; name: string } for sending files. |
| OnboardingStep | Step in the onboarding flow with id, label, instructions, status, and complete(). |
| AttachmentLike | Minimal shape for attachment validation utilities. |
| DownloadableAttachment | Extends AttachmentLike with url and contentType for downloading. |
| DownloadedAttachment | Result of downloadAttachment(): buffer, filename, contentType. |
| ManagerEvents | Lifecycle event signatures for ConnectorManager. |
Requirements
- Node.js >= 20
- TypeScript >= 5.5 (for development)
- discord.js ^14.25 (included as a dependency)
License
ISC
