@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/dist/index.cjs ADDED
@@ -0,0 +1,766 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Bot: () => Bot,
24
+ BotOnboarding: () => BotOnboarding,
25
+ ConnectorManager: () => ConnectorManager,
26
+ DEFAULT_SENT_MESSAGE_CACHE_SIZE: () => DEFAULT_SENT_MESSAGE_CACHE_SIZE,
27
+ DISCORD_MAX_MESSAGE_LENGTH: () => DISCORD_MAX_MESSAGE_LENGTH,
28
+ EventBuffer: () => EventBuffer,
29
+ MAX_ATTACHMENT_BYTES: () => MAX_ATTACHMENT_BYTES,
30
+ chunkMessage: () => chunkMessage,
31
+ downloadAttachment: () => downloadAttachment,
32
+ sanitizeAttachmentName: () => sanitizeAttachmentName,
33
+ validateAttachmentSize: () => validateAttachmentSize
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/bot.ts
38
+ var import_discord = require("discord.js");
39
+ var import_promises = require("fs/promises");
40
+
41
+ // src/event-buffer.ts
42
+ var EventBuffer = class {
43
+ buffer = [];
44
+ resolve = null;
45
+ closed = false;
46
+ push(value) {
47
+ if (this.closed) return;
48
+ if (this.resolve) {
49
+ const r = this.resolve;
50
+ this.resolve = null;
51
+ r({ value, done: false });
52
+ } else {
53
+ this.buffer.push(value);
54
+ }
55
+ }
56
+ close() {
57
+ this.closed = true;
58
+ if (this.resolve) {
59
+ const r = this.resolve;
60
+ this.resolve = null;
61
+ r({ value: void 0, done: true });
62
+ }
63
+ }
64
+ [Symbol.asyncIterator]() {
65
+ return {
66
+ next: () => {
67
+ if (this.buffer.length > 0) {
68
+ const value = this.buffer.shift();
69
+ return Promise.resolve({ value, done: false });
70
+ }
71
+ if (this.closed) {
72
+ return Promise.resolve({
73
+ value: void 0,
74
+ done: true
75
+ });
76
+ }
77
+ return new Promise((resolve) => {
78
+ this.resolve = resolve;
79
+ });
80
+ }
81
+ };
82
+ }
83
+ };
84
+
85
+ // src/message-utils.ts
86
+ var DISCORD_MAX_MESSAGE_LENGTH = 2e3;
87
+ function chunkMessage(text, limit = DISCORD_MAX_MESSAGE_LENGTH) {
88
+ if (!text) return [];
89
+ if (text.length <= limit) return [text];
90
+ const chunks = [];
91
+ const halfLimit = limit / 2;
92
+ let rest = text;
93
+ while (rest.length > limit) {
94
+ const para = rest.lastIndexOf("\n\n", limit);
95
+ const line = rest.lastIndexOf("\n", limit);
96
+ const space = rest.lastIndexOf(" ", limit);
97
+ const cut = para > halfLimit ? para : line > halfLimit ? line : space > 0 ? space : limit;
98
+ chunks.push(rest.slice(0, cut));
99
+ rest = rest.slice(cut).replace(/^\n+/, "");
100
+ }
101
+ if (rest) chunks.push(rest);
102
+ return chunks;
103
+ }
104
+
105
+ // src/attachment-utils.ts
106
+ var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
107
+ function validateAttachmentSize(attachment) {
108
+ if (attachment.size > MAX_ATTACHMENT_BYTES) {
109
+ const actualMB = (attachment.size / 1024 / 1024).toFixed(1);
110
+ const limitMB = MAX_ATTACHMENT_BYTES / 1024 / 1024;
111
+ const nameInfo = attachment.name ? `: ${attachment.name}` : "";
112
+ throw new Error(
113
+ `Attachment too large (${actualMB} MB > ${String(limitMB)} MB limit)${nameInfo}`
114
+ );
115
+ }
116
+ }
117
+ var UNSAFE_CHARS = /[[\]\r\n;]/g;
118
+ function sanitizeAttachmentName(attachment) {
119
+ const raw = attachment.name;
120
+ if (!raw || raw.replace(UNSAFE_CHARS, "").length === 0) {
121
+ return attachment.id;
122
+ }
123
+ return raw.replace(UNSAFE_CHARS, "_");
124
+ }
125
+ async function downloadAttachment(attachment) {
126
+ validateAttachmentSize(attachment);
127
+ const res = await fetch(attachment.url);
128
+ if (!res.ok) {
129
+ throw new Error(
130
+ `Failed to download attachment: ${String(res.status)} ${res.statusText}`
131
+ );
132
+ }
133
+ const ab = await res.arrayBuffer();
134
+ const buffer = Buffer.from(ab, 0, ab.byteLength);
135
+ const filename = sanitizeAttachmentName(attachment);
136
+ const contentType = attachment.contentType ?? "application/octet-stream";
137
+ return { buffer, filename, contentType };
138
+ }
139
+
140
+ // src/bot.ts
141
+ var DEFAULT_SENT_MESSAGE_CACHE_SIZE = 1e3;
142
+ var DEFAULT_INTENTS = [
143
+ import_discord.GatewayIntentBits.Guilds,
144
+ import_discord.GatewayIntentBits.GuildMessages,
145
+ import_discord.GatewayIntentBits.MessageContent,
146
+ import_discord.GatewayIntentBits.DirectMessages
147
+ ];
148
+ var Bot = class _Bot {
149
+ id;
150
+ name;
151
+ _status;
152
+ client;
153
+ sentMessages;
154
+ _connectedAt = null;
155
+ mentionHandlers = [];
156
+ replyHandlers = [];
157
+ channelSubscriptions = /* @__PURE__ */ new Map();
158
+ errorHandlers = [];
159
+ _includeBotMessages;
160
+ constructor(id, name, client, sentMessageCacheSize, includeBotMessages) {
161
+ this.id = id;
162
+ this.name = name;
163
+ this.client = client;
164
+ this.sentMessages = new import_discord.LimitedCollection({ maxSize: sentMessageCacheSize });
165
+ this._includeBotMessages = includeBotMessages;
166
+ this._status = "connecting";
167
+ }
168
+ get includeBotMessages() {
169
+ return this._includeBotMessages;
170
+ }
171
+ get status() {
172
+ return this._status;
173
+ }
174
+ get guildCount() {
175
+ return this.client.guilds.cache.size;
176
+ }
177
+ get connectedAt() {
178
+ return this._connectedAt;
179
+ }
180
+ /**
181
+ * Fast path: create a bot from a complete config, connect, verify, and
182
+ * return a ready Bot — or throw with diagnostics.
183
+ */
184
+ static async fromConfig(config) {
185
+ const cacheSize = config.sentMessageCacheSize ?? DEFAULT_SENT_MESSAGE_CACHE_SIZE;
186
+ if (cacheSize < 1 || !Number.isInteger(cacheSize)) {
187
+ throw new Error(
188
+ `sentMessageCacheSize must be a positive integer, got ${String(cacheSize)}`
189
+ );
190
+ }
191
+ const includeBotMessages = config.includeBotMessages ?? false;
192
+ const client = new import_discord.Client({
193
+ intents: config.intents ?? [...DEFAULT_INTENTS]
194
+ });
195
+ const bot = new _Bot(config.id, config.name, client, cacheSize, includeBotMessages);
196
+ await bot.connect(config.token);
197
+ bot.verify();
198
+ bot.setupEventHandlers();
199
+ bot._status = "ready";
200
+ bot._connectedAt = /* @__PURE__ */ new Date();
201
+ return bot;
202
+ }
203
+ // --- Sending ---
204
+ async send(channelId, content) {
205
+ const channel = await this.fetchTextChannel(channelId);
206
+ if (!("send" in channel)) {
207
+ throw new Error(`Channel ${channelId} is not a sendable channel`);
208
+ }
209
+ return this.sendPayloads(
210
+ content,
211
+ (payload) => channel.send(payload)
212
+ );
213
+ }
214
+ async sendDM(userId, content) {
215
+ const user = await this.client.users.fetch(userId);
216
+ return this.sendPayloads(
217
+ content,
218
+ (payload) => user.send(payload)
219
+ );
220
+ }
221
+ async reply(channelId, messageId, content) {
222
+ const channel = await this.fetchTextChannel(channelId);
223
+ if (!("send" in channel)) {
224
+ throw new Error(`Channel ${channelId} is not a sendable channel`);
225
+ }
226
+ let message;
227
+ try {
228
+ message = await channel.messages.fetch(messageId);
229
+ } catch (error) {
230
+ throw new Error(
231
+ `Cannot reply to message ${messageId}: ${error instanceof Error ? error.message : String(error)}`
232
+ );
233
+ }
234
+ return this.sendPayloads(content, (payload, i) => {
235
+ if (i === 0) {
236
+ return message.reply(payload);
237
+ }
238
+ return channel.send(payload);
239
+ });
240
+ }
241
+ // --- Channel operations ---
242
+ async react(channelId, messageId, emoji) {
243
+ const channel = await this.fetchTextChannel(channelId);
244
+ try {
245
+ const message = await channel.messages.fetch(messageId);
246
+ await message.react(emoji);
247
+ } catch (error) {
248
+ throw new Error(
249
+ `Cannot react to message ${messageId}: ${error instanceof Error ? error.message : String(error)}`
250
+ );
251
+ }
252
+ }
253
+ async editMessage(channelId, messageId, content) {
254
+ const channel = await this.fetchTextChannel(channelId);
255
+ try {
256
+ const message = await channel.messages.fetch(messageId);
257
+ await message.edit(content);
258
+ } catch (error) {
259
+ throw new Error(
260
+ `Cannot edit message ${messageId}: ${error instanceof Error ? error.message : String(error)}`
261
+ );
262
+ }
263
+ }
264
+ async fetchMessages(channelId, limit) {
265
+ const channel = await this.fetchTextChannel(channelId);
266
+ const clamped = Math.max(1, Math.min(limit ?? 20, 100));
267
+ const messages = await channel.messages.fetch({ limit: clamped });
268
+ return [...messages.values()].reverse().map((m) => this.toFetchedMessage(m));
269
+ }
270
+ async *fetchHistory(channelId, options) {
271
+ const { after, before, limit = 500 } = options;
272
+ if (after && before) {
273
+ throw new Error('Cannot specify both "after" and "before"');
274
+ }
275
+ let cursor;
276
+ let direction;
277
+ if (after) {
278
+ cursor = after;
279
+ direction = "after";
280
+ } else if (before) {
281
+ cursor = before;
282
+ direction = "before";
283
+ } else {
284
+ throw new Error('At least one of "after" or "before" must be specified');
285
+ }
286
+ const effectiveLimit = Math.min(limit, 5e3);
287
+ const channel = await this.fetchTextChannel(channelId);
288
+ let totalFetched = 0;
289
+ while (totalFetched < effectiveLimit) {
290
+ const pageSize = Math.min(100, effectiveLimit - totalFetched);
291
+ const fetched = await this.fetchPageWithRetry(channel, {
292
+ limit: pageSize,
293
+ [direction]: cursor
294
+ });
295
+ if (fetched.size === 0) break;
296
+ const rawMessages = [...fetched.values()];
297
+ const messages = rawMessages.map((m) => this.toFetchedMessage(m));
298
+ yield messages;
299
+ totalFetched += messages.length;
300
+ const lastMessage = rawMessages.at(-1);
301
+ if (!lastMessage) break;
302
+ cursor = lastMessage.id;
303
+ if (fetched.size < pageSize) break;
304
+ }
305
+ }
306
+ async getMessageAttachments(channelId, messageId) {
307
+ const channel = await this.fetchTextChannel(channelId);
308
+ try {
309
+ const message = await channel.messages.fetch(messageId);
310
+ return [...message.attachments.values()].map((att) => ({
311
+ id: att.id,
312
+ size: att.size,
313
+ name: att.name,
314
+ url: att.url,
315
+ contentType: att.contentType
316
+ }));
317
+ } catch (error) {
318
+ throw new Error(
319
+ `Cannot fetch attachments for message ${messageId}: ${error instanceof Error ? error.message : String(error)}`
320
+ );
321
+ }
322
+ }
323
+ // --- Receiving (event-driven) ---
324
+ onMention(handler) {
325
+ this.mentionHandlers.push(handler);
326
+ }
327
+ onReply(handler) {
328
+ this.replyHandlers.push(handler);
329
+ }
330
+ onMessage(channelId, handlerOrOpts, maybeHandler) {
331
+ let filter;
332
+ let handler;
333
+ if (typeof handlerOrOpts === "function") {
334
+ handler = handlerOrOpts;
335
+ } else {
336
+ filter = handlerOrOpts.filter;
337
+ if (!maybeHandler) {
338
+ throw new Error("Handler is required when providing filter options");
339
+ }
340
+ handler = maybeHandler;
341
+ }
342
+ const subs = this.channelSubscriptions.get(channelId) ?? [];
343
+ subs.push({ filter, handler });
344
+ this.channelSubscriptions.set(channelId, subs);
345
+ }
346
+ // --- Receiving (async iterable) ---
347
+ mentions() {
348
+ const buffer = new EventBuffer();
349
+ this.onMention((event) => {
350
+ buffer.push(event);
351
+ });
352
+ return buffer;
353
+ }
354
+ replies() {
355
+ const buffer = new EventBuffer();
356
+ this.onReply((event, original) => {
357
+ buffer.push([event, original]);
358
+ });
359
+ return buffer;
360
+ }
361
+ messages(channelId, options) {
362
+ const buffer = new EventBuffer();
363
+ if (options?.filter) {
364
+ this.onMessage(channelId, { filter: options.filter }, (event) => {
365
+ buffer.push(event);
366
+ });
367
+ } else {
368
+ this.onMessage(channelId, (event) => {
369
+ buffer.push(event);
370
+ });
371
+ }
372
+ return buffer;
373
+ }
374
+ // --- Error handling ---
375
+ on(_event, handler) {
376
+ this.errorHandlers.push(handler);
377
+ }
378
+ emitError(error) {
379
+ const err = error instanceof Error ? error : new Error(String(error));
380
+ for (const handler of this.errorHandlers) {
381
+ handler(err);
382
+ }
383
+ }
384
+ // --- Lifecycle ---
385
+ async disconnect() {
386
+ this.client.removeAllListeners();
387
+ await this.client.destroy();
388
+ this._status = "disconnected";
389
+ this._connectedAt = null;
390
+ }
391
+ // --- Private ---
392
+ async fetchTextChannel(channelId) {
393
+ const channel = await this.client.channels.fetch(channelId);
394
+ if (!channel?.isTextBased()) {
395
+ throw new Error(
396
+ `Channel ${channelId} is not a text-based channel or does not exist`
397
+ );
398
+ }
399
+ return channel;
400
+ }
401
+ toFetchedMessage(msg) {
402
+ return {
403
+ messageId: msg.id,
404
+ channelId: msg.channelId,
405
+ author: {
406
+ id: msg.author.id,
407
+ username: msg.author.username,
408
+ bot: msg.author.bot
409
+ },
410
+ content: msg.content,
411
+ timestamp: msg.createdAt,
412
+ attachmentCount: msg.attachments.size
413
+ };
414
+ }
415
+ async fetchPageWithRetry(channel, options, maxRetryMs = 5e3) {
416
+ try {
417
+ return await channel.messages.fetch(options);
418
+ } catch (err) {
419
+ if (err instanceof Error && "retryAfter" in err) {
420
+ const { retryAfter } = err;
421
+ if (typeof retryAfter === "number" && retryAfter <= maxRetryMs) {
422
+ await new Promise((resolve) => setTimeout(resolve, retryAfter));
423
+ return channel.messages.fetch(options);
424
+ }
425
+ }
426
+ throw err;
427
+ }
428
+ }
429
+ async connect(token) {
430
+ this._status = "connecting";
431
+ await this.client.login(token);
432
+ }
433
+ verify() {
434
+ this._status = "verifying";
435
+ if (this.client.guilds.cache.size === 0) {
436
+ this._status = "failed";
437
+ throw new Error(
438
+ "Bot is not in any guild. Invite the bot to at least one server before connecting."
439
+ );
440
+ }
441
+ }
442
+ setupEventHandlers() {
443
+ this.client.on(import_discord.Events.MessageCreate, (message) => {
444
+ try {
445
+ if (message.author.id === this.client.user?.id) return;
446
+ if (message.author.bot && !this._includeBotMessages) return;
447
+ const event = this.toMessageEvent(message);
448
+ const botUser = this.client.user;
449
+ if (botUser && message.mentions.has(botUser.id)) {
450
+ for (const handler of this.mentionHandlers) {
451
+ handler(event);
452
+ }
453
+ }
454
+ if (message.reference?.messageId) {
455
+ const original = this.sentMessages.get(message.reference.messageId);
456
+ if (original) {
457
+ for (const handler of this.replyHandlers) {
458
+ handler(event, original);
459
+ }
460
+ }
461
+ }
462
+ const subs = this.channelSubscriptions.get(message.channelId);
463
+ if (subs) {
464
+ for (const sub of subs) {
465
+ if (sub.filter && !sub.filter(event)) continue;
466
+ sub.handler(event);
467
+ }
468
+ }
469
+ } catch (error) {
470
+ this.emitError(error);
471
+ }
472
+ });
473
+ }
474
+ toMessageEvent(message) {
475
+ return {
476
+ messageId: message.id,
477
+ author: {
478
+ id: message.author.id,
479
+ username: message.author.username,
480
+ bot: message.author.bot
481
+ },
482
+ content: message.content,
483
+ channelId: message.channelId,
484
+ guildId: message.guildId ?? null,
485
+ timestamp: message.createdAt,
486
+ mentions: [...message.mentions.users.keys()],
487
+ raw: message
488
+ };
489
+ }
490
+ async sendPayloads(content, dispatcher) {
491
+ const files = typeof content !== "string" && content.files?.length ? await validateAndBuildAttachments(content.files) : void 0;
492
+ const payloads = toChunkedPayloads(content, files);
493
+ const results = [];
494
+ let i = 0;
495
+ for (const payload of payloads) {
496
+ const msg = await dispatcher(payload, i++);
497
+ results.push(this.trackSentMessage(msg));
498
+ }
499
+ return results;
500
+ }
501
+ trackSentMessage(message) {
502
+ const sent = {
503
+ messageId: message.id,
504
+ channelId: message.channelId,
505
+ timestamp: message.createdAt,
506
+ raw: message
507
+ };
508
+ this.sentMessages.set(sent.messageId, sent);
509
+ return sent;
510
+ }
511
+ };
512
+ function toChunkedPayloads(content, files) {
513
+ if (typeof content === "string") {
514
+ const chunks2 = chunkMessage(content);
515
+ return chunks2.map((text2, i) => ({
516
+ content: text2,
517
+ ...i === 0 && files?.length ? { files } : {}
518
+ }));
519
+ }
520
+ const text = content.content ?? "";
521
+ const chunks = chunkMessage(text);
522
+ if (chunks.length === 0) {
523
+ return [{ embeds: content.embeds, ...files?.length ? { files } : {} }];
524
+ }
525
+ return chunks.map((chunk, i) => ({
526
+ content: chunk,
527
+ ...i === 0 && content.embeds ? { embeds: content.embeds } : {},
528
+ ...i === 0 && files?.length ? { files } : {}
529
+ }));
530
+ }
531
+ var MAX_FILES_PER_MESSAGE = 10;
532
+ async function validateAndBuildAttachments(files) {
533
+ if (files.length > MAX_FILES_PER_MESSAGE) {
534
+ throw new Error(
535
+ `Too many attachments: ${String(files.length)} exceeds Discord's limit of ${String(MAX_FILES_PER_MESSAGE)} attachments per message`
536
+ );
537
+ }
538
+ const sizes = await Promise.all(
539
+ files.map(
540
+ (file) => typeof file.data === "string" ? (0, import_promises.stat)(file.data).then((st) => st.size) : Promise.resolve(file.data.byteLength)
541
+ )
542
+ );
543
+ return files.map((file, i) => {
544
+ const size = sizes[i];
545
+ if (size === void 0) throw new Error(`Missing size for attachment: ${file.name}`);
546
+ validateAttachmentSize({ size, name: file.name, id: file.name });
547
+ return new import_discord.AttachmentBuilder(file.data, { name: file.name });
548
+ });
549
+ }
550
+
551
+ // src/onboarding.ts
552
+ var import_discord2 = require("discord.js");
553
+ function toErrorMessage(err) {
554
+ return err instanceof Error ? err.message : String(err);
555
+ }
556
+ var OnboardingStepImpl = class {
557
+ id;
558
+ label;
559
+ instructions;
560
+ status;
561
+ error;
562
+ nextSibling;
563
+ _action;
564
+ _dependencies;
565
+ constructor(id, label, instructions, action, dependencies = [], initialStatus = "pending") {
566
+ this.id = id;
567
+ this.label = label;
568
+ this.instructions = instructions;
569
+ this._action = action;
570
+ this._dependencies = dependencies;
571
+ this.status = initialStatus;
572
+ }
573
+ async complete(...args) {
574
+ for (const dep of this._dependencies) {
575
+ if (dep.status !== "completed") {
576
+ const error = `Cannot complete '${this.id}': dependency '${dep.id}' is not completed.`;
577
+ this.status = "failed";
578
+ this.error = error;
579
+ return { success: false, error };
580
+ }
581
+ }
582
+ try {
583
+ const result = await this._action(...args);
584
+ if (result.success) {
585
+ this.status = "completed";
586
+ this.error = void 0;
587
+ if (this.nextSibling?.status === "pending") {
588
+ this.nextSibling.status = "ready";
589
+ result.nextStep = this.nextSibling;
590
+ }
591
+ } else {
592
+ this.status = "failed";
593
+ this.error = result.error;
594
+ }
595
+ return result;
596
+ } catch (err) {
597
+ const message = toErrorMessage(err);
598
+ this.status = "failed";
599
+ this.error = message;
600
+ return { success: false, error: message };
601
+ }
602
+ }
603
+ };
604
+ var BotOnboarding = class {
605
+ id;
606
+ name;
607
+ steps;
608
+ _bot;
609
+ _token;
610
+ constructor(id, name) {
611
+ this.id = id;
612
+ this.name = name;
613
+ const provideToken = new OnboardingStepImpl(
614
+ "provide_token",
615
+ "Provide Bot Token",
616
+ "Enter your Discord bot token. You can find this in the Discord Developer Portal under your application's Bot settings.",
617
+ async (...args) => {
618
+ const token = args[0];
619
+ const client = new import_discord2.Client({ intents: [...DEFAULT_INTENTS] });
620
+ try {
621
+ await client.login(token);
622
+ await client.destroy();
623
+ this._token = token;
624
+ return { success: true };
625
+ } catch (err) {
626
+ return { success: false, error: toErrorMessage(err) };
627
+ }
628
+ },
629
+ [],
630
+ "ready"
631
+ );
632
+ const inviteToServer = new OnboardingStepImpl(
633
+ "invite_to_server",
634
+ "Invite Bot to Server",
635
+ "Add the bot to at least one Discord server using the OAuth2 URL from the Developer Portal.",
636
+ async () => {
637
+ if (!this._token) {
638
+ return {
639
+ success: false,
640
+ error: "Token not set \u2014 complete the provide_token step first."
641
+ };
642
+ }
643
+ const client = new import_discord2.Client({ intents: [...DEFAULT_INTENTS] });
644
+ try {
645
+ await client.login(this._token);
646
+ if (client.guilds.cache.size === 0) {
647
+ await client.destroy();
648
+ return { success: false, error: "Bot is not in any server yet" };
649
+ }
650
+ await client.destroy();
651
+ return { success: true };
652
+ } catch (err) {
653
+ return { success: false, error: toErrorMessage(err) };
654
+ }
655
+ },
656
+ [provideToken]
657
+ );
658
+ const verifyPermissions = new OnboardingStepImpl(
659
+ "verify_permissions",
660
+ "Verify Permissions",
661
+ "Verify the bot has the required permissions in your server.",
662
+ async () => {
663
+ if (!this._token) {
664
+ return {
665
+ success: false,
666
+ error: "Token not set \u2014 complete the provide_token step first."
667
+ };
668
+ }
669
+ try {
670
+ this._bot = await Bot.fromConfig({
671
+ id: this.id,
672
+ name: this.name,
673
+ token: this._token
674
+ });
675
+ return { success: true };
676
+ } catch (err) {
677
+ return { success: false, error: toErrorMessage(err) };
678
+ }
679
+ },
680
+ [inviteToServer]
681
+ );
682
+ provideToken.nextSibling = inviteToServer;
683
+ inviteToServer.nextSibling = verifyPermissions;
684
+ this.steps = [provideToken, inviteToServer, verifyPermissions];
685
+ }
686
+ get bot() {
687
+ return this._bot;
688
+ }
689
+ };
690
+
691
+ // src/connector-manager.ts
692
+ var ConnectorManager = class {
693
+ botMap = /* @__PURE__ */ new Map();
694
+ mentionHandlers = [];
695
+ lifecycleHandlers = /* @__PURE__ */ new Map();
696
+ addBot(bot) {
697
+ if (this.botMap.has(bot.id)) {
698
+ throw new Error(`Bot with id "${bot.id}" already exists in this manager.`);
699
+ }
700
+ this.botMap.set(bot.id, bot);
701
+ bot.onMention((event) => {
702
+ for (const handler of this.mentionHandlers) {
703
+ handler(event, bot);
704
+ }
705
+ });
706
+ bot.on("error", (error) => {
707
+ this.emitLifecycle("botError", bot, error);
708
+ });
709
+ }
710
+ getBot(id) {
711
+ return this.botMap.get(id);
712
+ }
713
+ get bots() {
714
+ return this.botMap.values();
715
+ }
716
+ removeBot(id) {
717
+ this.botMap.delete(id);
718
+ }
719
+ status() {
720
+ return [...this.botMap.values()].map((bot) => ({
721
+ id: bot.id,
722
+ name: bot.name,
723
+ status: bot.status
724
+ }));
725
+ }
726
+ onMention(callback) {
727
+ this.mentionHandlers.push(callback);
728
+ }
729
+ on(event, callback) {
730
+ const handlers = this.lifecycleHandlers.get(event) ?? [];
731
+ handlers.push(callback);
732
+ this.lifecycleHandlers.set(event, handlers);
733
+ }
734
+ emitLifecycle(event, ...args) {
735
+ const handlers = this.lifecycleHandlers.get(event);
736
+ if (handlers) {
737
+ for (const handler of handlers) {
738
+ handler(...args);
739
+ }
740
+ }
741
+ }
742
+ onboardBot(id, name) {
743
+ return new BotOnboarding(id, name);
744
+ }
745
+ async disconnectAll() {
746
+ await Promise.all([...this.botMap.values()].map((bot) => bot.disconnect()));
747
+ }
748
+ async shutdown() {
749
+ await this.disconnectAll();
750
+ }
751
+ };
752
+ // Annotate the CommonJS export names for ESM import in node:
753
+ 0 && (module.exports = {
754
+ Bot,
755
+ BotOnboarding,
756
+ ConnectorManager,
757
+ DEFAULT_SENT_MESSAGE_CACHE_SIZE,
758
+ DISCORD_MAX_MESSAGE_LENGTH,
759
+ EventBuffer,
760
+ MAX_ATTACHMENT_BYTES,
761
+ chunkMessage,
762
+ downloadAttachment,
763
+ sanitizeAttachmentName,
764
+ validateAttachmentSize
765
+ });
766
+ //# sourceMappingURL=index.cjs.map