@furlow/discord 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.js ADDED
@@ -0,0 +1,1089 @@
1
+ // src/client/index.ts
2
+ import {
3
+ Client,
4
+ GatewayIntentBits,
5
+ Partials
6
+ } from "discord.js";
7
+ var INTENT_MAP = {
8
+ guilds: GatewayIntentBits.Guilds,
9
+ guild_members: GatewayIntentBits.GuildMembers,
10
+ guild_moderation: GatewayIntentBits.GuildModeration,
11
+ guild_emojis_and_stickers: GatewayIntentBits.GuildEmojisAndStickers,
12
+ guild_integrations: GatewayIntentBits.GuildIntegrations,
13
+ guild_webhooks: GatewayIntentBits.GuildWebhooks,
14
+ guild_invites: GatewayIntentBits.GuildInvites,
15
+ guild_voice_states: GatewayIntentBits.GuildVoiceStates,
16
+ guild_presences: GatewayIntentBits.GuildPresences,
17
+ guild_messages: GatewayIntentBits.GuildMessages,
18
+ guild_message_reactions: GatewayIntentBits.GuildMessageReactions,
19
+ guild_message_typing: GatewayIntentBits.GuildMessageTyping,
20
+ direct_messages: GatewayIntentBits.DirectMessages,
21
+ direct_message_reactions: GatewayIntentBits.DirectMessageReactions,
22
+ direct_message_typing: GatewayIntentBits.DirectMessageTyping,
23
+ message_content: GatewayIntentBits.MessageContent,
24
+ guild_scheduled_events: GatewayIntentBits.GuildScheduledEvents,
25
+ auto_moderation_configuration: GatewayIntentBits.AutoModerationConfiguration,
26
+ auto_moderation_execution: GatewayIntentBits.AutoModerationExecution
27
+ };
28
+ var FurlowClient = class {
29
+ client;
30
+ token;
31
+ spec;
32
+ constructor(options) {
33
+ this.token = options.token;
34
+ this.spec = options.spec;
35
+ const intents = this.resolveIntents(options.spec.intents);
36
+ const clientOptions = {
37
+ intents,
38
+ partials: [
39
+ Partials.Message,
40
+ Partials.Channel,
41
+ Partials.Reaction,
42
+ Partials.User,
43
+ Partials.GuildMember
44
+ ]
45
+ };
46
+ this.client = new Client(clientOptions);
47
+ }
48
+ /**
49
+ * Resolve intents from spec
50
+ */
51
+ resolveIntents(config) {
52
+ if (!config) {
53
+ return [
54
+ GatewayIntentBits.Guilds,
55
+ GatewayIntentBits.GuildMessages,
56
+ GatewayIntentBits.GuildMembers,
57
+ GatewayIntentBits.MessageContent
58
+ ];
59
+ }
60
+ if (config.auto) {
61
+ return this.autoDetectIntents();
62
+ }
63
+ if (config.explicit) {
64
+ return config.explicit.map((intent) => INTENT_MAP[intent]).filter((i) => i !== void 0);
65
+ }
66
+ return [GatewayIntentBits.Guilds];
67
+ }
68
+ /**
69
+ * Auto-detect required intents from spec
70
+ */
71
+ autoDetectIntents() {
72
+ const intents = /* @__PURE__ */ new Set();
73
+ intents.add(GatewayIntentBits.Guilds);
74
+ if (this.spec.events) {
75
+ for (const handler of this.spec.events) {
76
+ switch (handler.event) {
77
+ case "message_create":
78
+ case "message":
79
+ case "message_update":
80
+ case "message_delete":
81
+ intents.add(GatewayIntentBits.GuildMessages);
82
+ intents.add(GatewayIntentBits.MessageContent);
83
+ break;
84
+ case "guild_member_add":
85
+ case "guild_member_remove":
86
+ case "guild_member_update":
87
+ case "member_join":
88
+ case "member_leave":
89
+ intents.add(GatewayIntentBits.GuildMembers);
90
+ break;
91
+ case "voice_state_update":
92
+ case "voice_join":
93
+ case "voice_leave":
94
+ intents.add(GatewayIntentBits.GuildVoiceStates);
95
+ break;
96
+ case "message_reaction_add":
97
+ case "message_reaction_remove":
98
+ intents.add(GatewayIntentBits.GuildMessageReactions);
99
+ break;
100
+ case "presence_update":
101
+ intents.add(GatewayIntentBits.GuildPresences);
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ if (this.spec.commands?.length) {
107
+ intents.add(GatewayIntentBits.GuildMessages);
108
+ }
109
+ if (this.spec.voice) {
110
+ intents.add(GatewayIntentBits.GuildVoiceStates);
111
+ }
112
+ return [...intents];
113
+ }
114
+ /**
115
+ * Start the client
116
+ */
117
+ async start() {
118
+ await this.client.login(this.token);
119
+ if (!this.client.isReady()) {
120
+ await new Promise((resolve) => {
121
+ this.client.once("ready", () => resolve());
122
+ });
123
+ }
124
+ await this.applyIdentity();
125
+ await this.applyPresence();
126
+ }
127
+ /**
128
+ * Stop the client
129
+ */
130
+ async stop() {
131
+ await this.client.destroy();
132
+ }
133
+ /**
134
+ * Apply bot identity
135
+ */
136
+ async applyIdentity() {
137
+ if (!this.spec.identity) return;
138
+ const identity = this.spec.identity;
139
+ if (identity.name && this.client.user?.username !== identity.name) {
140
+ try {
141
+ await this.client.user?.setUsername(identity.name);
142
+ } catch {
143
+ }
144
+ }
145
+ if (identity.avatar) {
146
+ try {
147
+ await this.client.user?.setAvatar(identity.avatar);
148
+ } catch {
149
+ }
150
+ }
151
+ }
152
+ /**
153
+ * Apply presence
154
+ */
155
+ async applyPresence() {
156
+ if (!this.spec.presence) return;
157
+ const presence = this.spec.presence;
158
+ this.client.user?.setPresence({
159
+ status: presence.status ?? "online",
160
+ activities: presence.activity ? [
161
+ {
162
+ type: this.getActivityType(presence.activity.type),
163
+ name: presence.activity.text,
164
+ url: presence.activity.url,
165
+ state: presence.activity.state
166
+ }
167
+ ] : void 0
168
+ });
169
+ }
170
+ /**
171
+ * Get Discord.js activity type
172
+ */
173
+ getActivityType(type) {
174
+ const types = {
175
+ playing: 0,
176
+ streaming: 1,
177
+ listening: 2,
178
+ watching: 3,
179
+ custom: 4,
180
+ competing: 5
181
+ };
182
+ return types[type] ?? 0;
183
+ }
184
+ /**
185
+ * Register event listener
186
+ */
187
+ on(event, listener) {
188
+ this.client.on(event, listener);
189
+ return this;
190
+ }
191
+ /**
192
+ * Register one-time event listener
193
+ */
194
+ once(event, listener) {
195
+ this.client.once(event, listener);
196
+ return this;
197
+ }
198
+ /**
199
+ * Get the underlying Discord.js client
200
+ */
201
+ getClient() {
202
+ return this.client;
203
+ }
204
+ /**
205
+ * Get the spec
206
+ */
207
+ getSpec() {
208
+ return this.spec;
209
+ }
210
+ /**
211
+ * Check if client is ready
212
+ */
213
+ isReady() {
214
+ return this.client.isReady();
215
+ }
216
+ /**
217
+ * Get guild count
218
+ */
219
+ get guildCount() {
220
+ return this.client.guilds.cache.size;
221
+ }
222
+ /**
223
+ * Get user count (approximate)
224
+ */
225
+ get userCount() {
226
+ return this.client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0);
227
+ }
228
+ };
229
+ function createClient(options) {
230
+ return new FurlowClient(options);
231
+ }
232
+
233
+ // src/gateway/index.ts
234
+ var GatewayManager = class {
235
+ client;
236
+ config;
237
+ reconnectAttempts = 0;
238
+ maxReconnectAttempts;
239
+ baseDelay;
240
+ maxDelay;
241
+ backoffStrategy;
242
+ constructor(client, options = {}) {
243
+ this.client = client;
244
+ this.config = options.config ?? {};
245
+ const reconnect = this.config.reconnect ?? {};
246
+ this.maxReconnectAttempts = reconnect.max_retries ?? 10;
247
+ const baseDelay = reconnect.base_delay ?? "1s";
248
+ const maxDelay = reconnect.max_delay ?? "60s";
249
+ this.baseDelay = typeof baseDelay === "number" ? baseDelay : this.parseDuration(baseDelay);
250
+ this.maxDelay = typeof maxDelay === "number" ? maxDelay : this.parseDuration(maxDelay);
251
+ this.backoffStrategy = reconnect.backoff ?? "exponential";
252
+ this.setupListeners(options);
253
+ }
254
+ setupListeners(options) {
255
+ this.client.on("shardReady", (shardId) => {
256
+ console.log(`Shard ${shardId} ready`);
257
+ this.reconnectAttempts = 0;
258
+ });
259
+ this.client.on("shardDisconnect", (event, shardId) => {
260
+ console.log(`Shard ${shardId} disconnected: ${event.code}`);
261
+ options.onDisconnect?.();
262
+ });
263
+ this.client.on("shardReconnecting", (shardId) => {
264
+ console.log(`Shard ${shardId} reconnecting...`);
265
+ this.reconnectAttempts++;
266
+ options.onReconnect?.();
267
+ });
268
+ this.client.on("shardError", (error, shardId) => {
269
+ console.error(`Shard ${shardId} error:`, error);
270
+ options.onError?.(error);
271
+ });
272
+ this.client.on("shardResume", (shardId, replayedEvents) => {
273
+ console.log(`Shard ${shardId} resumed, replayed ${replayedEvents} events`);
274
+ this.reconnectAttempts = 0;
275
+ });
276
+ }
277
+ /**
278
+ * Get the delay for the next reconnection attempt
279
+ */
280
+ getReconnectDelay() {
281
+ switch (this.backoffStrategy) {
282
+ case "exponential":
283
+ return Math.min(
284
+ this.baseDelay * Math.pow(2, this.reconnectAttempts),
285
+ this.maxDelay
286
+ );
287
+ case "linear":
288
+ return Math.min(
289
+ this.baseDelay * (this.reconnectAttempts + 1),
290
+ this.maxDelay
291
+ );
292
+ case "fixed":
293
+ default:
294
+ return this.baseDelay;
295
+ }
296
+ }
297
+ /**
298
+ * Check if we should attempt reconnection
299
+ */
300
+ shouldReconnect() {
301
+ return this.reconnectAttempts < this.maxReconnectAttempts;
302
+ }
303
+ /**
304
+ * Get current reconnect attempt count
305
+ */
306
+ getReconnectAttempts() {
307
+ return this.reconnectAttempts;
308
+ }
309
+ /**
310
+ * Reset reconnect counter
311
+ */
312
+ resetReconnectAttempts() {
313
+ this.reconnectAttempts = 0;
314
+ }
315
+ /**
316
+ * Get gateway latency (ping)
317
+ */
318
+ getPing() {
319
+ return this.client.ws.ping;
320
+ }
321
+ /**
322
+ * Get shard info
323
+ */
324
+ getShardInfo() {
325
+ return [...this.client.ws.shards.values()].map((shard) => ({
326
+ id: shard.id,
327
+ status: shard.status.toString(),
328
+ ping: shard.ping
329
+ }));
330
+ }
331
+ /**
332
+ * Parse duration string to milliseconds
333
+ */
334
+ parseDuration(duration) {
335
+ const match = duration.match(/^(\d+)(ms|s|m|h)?$/);
336
+ if (!match) return 1e3;
337
+ const value = parseInt(match[1], 10);
338
+ const unit = match[2] ?? "s";
339
+ switch (unit) {
340
+ case "ms":
341
+ return value;
342
+ case "s":
343
+ return value * 1e3;
344
+ case "m":
345
+ return value * 60 * 1e3;
346
+ case "h":
347
+ return value * 60 * 60 * 1e3;
348
+ default:
349
+ return value * 1e3;
350
+ }
351
+ }
352
+ };
353
+ function createGatewayManager(client, options) {
354
+ return new GatewayManager(client, options);
355
+ }
356
+
357
+ // src/interactions/index.ts
358
+ import {
359
+ REST,
360
+ Routes,
361
+ SlashCommandBuilder,
362
+ ContextMenuCommandBuilder,
363
+ ApplicationCommandType
364
+ } from "discord.js";
365
+ var InteractionHandler = class {
366
+ client;
367
+ rest;
368
+ clientId;
369
+ guildId;
370
+ commandHandlers = /* @__PURE__ */ new Map();
371
+ buttonHandlers = /* @__PURE__ */ new Map();
372
+ selectHandlers = /* @__PURE__ */ new Map();
373
+ modalHandlers = /* @__PURE__ */ new Map();
374
+ userContextHandlers = /* @__PURE__ */ new Map();
375
+ messageContextHandlers = /* @__PURE__ */ new Map();
376
+ constructor(options) {
377
+ this.client = options.client;
378
+ this.clientId = options.clientId;
379
+ this.guildId = options.guildId;
380
+ this.rest = new REST({ version: "10" }).setToken(options.token);
381
+ this.setupListener();
382
+ }
383
+ /**
384
+ * Set up the interaction listener
385
+ */
386
+ setupListener() {
387
+ this.client.on("interactionCreate", async (interaction) => {
388
+ try {
389
+ await this.handleInteraction(interaction);
390
+ } catch (error) {
391
+ console.error("Interaction error:", error);
392
+ if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) {
393
+ await interaction.reply({
394
+ content: "An error occurred while processing this interaction.",
395
+ ephemeral: true
396
+ }).catch(() => {
397
+ });
398
+ }
399
+ }
400
+ });
401
+ }
402
+ /**
403
+ * Handle an interaction
404
+ */
405
+ async handleInteraction(interaction) {
406
+ if (interaction.isChatInputCommand()) {
407
+ const handler = this.commandHandlers.get(interaction.commandName);
408
+ if (handler) {
409
+ await handler(interaction);
410
+ }
411
+ } else if (interaction.isButton()) {
412
+ const handler = this.buttonHandlers.get(interaction.customId) ?? this.findPrefixHandler(this.buttonHandlers, interaction.customId);
413
+ if (handler) {
414
+ await handler(interaction);
415
+ }
416
+ } else if (interaction.isStringSelectMenu()) {
417
+ const handler = this.selectHandlers.get(interaction.customId) ?? this.findPrefixHandler(this.selectHandlers, interaction.customId);
418
+ if (handler) {
419
+ await handler(interaction);
420
+ }
421
+ } else if (interaction.isModalSubmit()) {
422
+ const handler = this.modalHandlers.get(interaction.customId) ?? this.findPrefixHandler(this.modalHandlers, interaction.customId);
423
+ if (handler) {
424
+ await handler(interaction);
425
+ }
426
+ } else if (interaction.isUserContextMenuCommand()) {
427
+ const handler = this.userContextHandlers.get(interaction.commandName);
428
+ if (handler) {
429
+ await handler(interaction);
430
+ }
431
+ } else if (interaction.isMessageContextMenuCommand()) {
432
+ const handler = this.messageContextHandlers.get(interaction.commandName);
433
+ if (handler) {
434
+ await handler(interaction);
435
+ }
436
+ }
437
+ }
438
+ /**
439
+ * Find a handler by prefix (for dynamic IDs like "button_123")
440
+ */
441
+ findPrefixHandler(handlers, customId) {
442
+ for (const [key, handler] of handlers) {
443
+ if (key.endsWith("*") && customId.startsWith(key.slice(0, -1))) {
444
+ return handler;
445
+ }
446
+ }
447
+ return void 0;
448
+ }
449
+ /**
450
+ * Register a command handler
451
+ */
452
+ onCommand(name, handler) {
453
+ this.commandHandlers.set(name, handler);
454
+ }
455
+ /**
456
+ * Register a button handler
457
+ */
458
+ onButton(customId, handler) {
459
+ this.buttonHandlers.set(customId, handler);
460
+ }
461
+ /**
462
+ * Register a select menu handler
463
+ */
464
+ onSelect(customId, handler) {
465
+ this.selectHandlers.set(customId, handler);
466
+ }
467
+ /**
468
+ * Register a modal handler
469
+ */
470
+ onModal(customId, handler) {
471
+ this.modalHandlers.set(customId, handler);
472
+ }
473
+ /**
474
+ * Register a user context menu handler
475
+ */
476
+ onUserContext(name, handler) {
477
+ this.userContextHandlers.set(name, handler);
478
+ }
479
+ /**
480
+ * Register a message context menu handler
481
+ */
482
+ onMessageContext(name, handler) {
483
+ this.messageContextHandlers.set(name, handler);
484
+ }
485
+ /**
486
+ * Register slash commands with Discord
487
+ */
488
+ async registerCommands(commands, contextMenus) {
489
+ const slashCommands = commands.map((cmd) => this.buildSlashCommand(cmd));
490
+ const contextCommands = (contextMenus ?? []).map((cmd) => this.buildContextMenu(cmd));
491
+ const allCommands = [...slashCommands, ...contextCommands];
492
+ if (this.guildId) {
493
+ await this.rest.put(
494
+ Routes.applicationGuildCommands(this.clientId, this.guildId),
495
+ { body: allCommands }
496
+ );
497
+ } else {
498
+ await this.rest.put(Routes.applicationCommands(this.clientId), {
499
+ body: allCommands
500
+ });
501
+ }
502
+ }
503
+ /**
504
+ * Build a slash command from definition
505
+ */
506
+ buildSlashCommand(cmd) {
507
+ const builder = new SlashCommandBuilder().setName(cmd.name).setDescription(cmd.description);
508
+ if (cmd.dm_permission !== void 0) {
509
+ builder.setDMPermission(cmd.dm_permission);
510
+ }
511
+ if (cmd.nsfw) {
512
+ builder.setNSFW(true);
513
+ }
514
+ if (cmd.options) {
515
+ for (const opt of cmd.options) {
516
+ this.addOption(builder, opt);
517
+ }
518
+ }
519
+ if (cmd.subcommands) {
520
+ for (const sub of cmd.subcommands) {
521
+ builder.addSubcommand((subBuilder) => {
522
+ subBuilder.setName(sub.name).setDescription(sub.description);
523
+ if (sub.options) {
524
+ for (const opt of sub.options) {
525
+ this.addOption(subBuilder, opt);
526
+ }
527
+ }
528
+ return subBuilder;
529
+ });
530
+ }
531
+ }
532
+ if (cmd.subcommand_groups) {
533
+ for (const group of cmd.subcommand_groups) {
534
+ builder.addSubcommandGroup((groupBuilder) => {
535
+ groupBuilder.setName(group.name).setDescription(group.description);
536
+ for (const sub of group.subcommands) {
537
+ groupBuilder.addSubcommand((subBuilder) => {
538
+ subBuilder.setName(sub.name).setDescription(sub.description);
539
+ if (sub.options) {
540
+ for (const opt of sub.options) {
541
+ this.addOption(subBuilder, opt);
542
+ }
543
+ }
544
+ return subBuilder;
545
+ });
546
+ }
547
+ return groupBuilder;
548
+ });
549
+ }
550
+ }
551
+ return builder.toJSON();
552
+ }
553
+ /**
554
+ * Add an option to a command builder
555
+ */
556
+ addOption(builder, opt) {
557
+ const addMethod = `add${this.getOptionMethodName(opt.type)}Option`;
558
+ if (typeof builder[addMethod] !== "function") {
559
+ console.warn(`Unknown option type: ${opt.type}`);
560
+ return;
561
+ }
562
+ builder[addMethod]((optBuilder) => {
563
+ optBuilder.setName(opt.name).setDescription(opt.description);
564
+ if (opt.required) {
565
+ optBuilder.setRequired(true);
566
+ }
567
+ if (opt.choices && optBuilder.addChoices) {
568
+ optBuilder.addChoices(...opt.choices);
569
+ }
570
+ if (opt.min_value !== void 0 && optBuilder.setMinValue) {
571
+ optBuilder.setMinValue(opt.min_value);
572
+ }
573
+ if (opt.max_value !== void 0 && optBuilder.setMaxValue) {
574
+ optBuilder.setMaxValue(opt.max_value);
575
+ }
576
+ if (opt.min_length !== void 0 && optBuilder.setMinLength) {
577
+ optBuilder.setMinLength(opt.min_length);
578
+ }
579
+ if (opt.max_length !== void 0 && optBuilder.setMaxLength) {
580
+ optBuilder.setMaxLength(opt.max_length);
581
+ }
582
+ if (opt.autocomplete && optBuilder.setAutocomplete) {
583
+ optBuilder.setAutocomplete(true);
584
+ }
585
+ return optBuilder;
586
+ });
587
+ }
588
+ /**
589
+ * Get the method name for an option type
590
+ */
591
+ getOptionMethodName(type) {
592
+ const map = {
593
+ string: "String",
594
+ integer: "Integer",
595
+ number: "Number",
596
+ boolean: "Boolean",
597
+ user: "User",
598
+ channel: "Channel",
599
+ role: "Role",
600
+ mentionable: "Mentionable",
601
+ attachment: "Attachment"
602
+ };
603
+ return map[type] ?? "String";
604
+ }
605
+ /**
606
+ * Build a context menu command
607
+ */
608
+ buildContextMenu(cmd) {
609
+ const builder = new ContextMenuCommandBuilder().setName(cmd.name).setType(
610
+ cmd.type === "user" ? ApplicationCommandType.User : ApplicationCommandType.Message
611
+ );
612
+ return builder.toJSON();
613
+ }
614
+ };
615
+ function createInteractionHandler(options) {
616
+ return new InteractionHandler(options);
617
+ }
618
+
619
+ // src/voice/index.ts
620
+ import {
621
+ joinVoiceChannel,
622
+ createAudioPlayer,
623
+ createAudioResource,
624
+ AudioPlayerStatus,
625
+ VoiceConnectionStatus,
626
+ entersState,
627
+ getVoiceConnection
628
+ } from "@discordjs/voice";
629
+ var VoiceManager = class {
630
+ guildStates = /* @__PURE__ */ new Map();
631
+ config = {};
632
+ /**
633
+ * Configure the voice manager
634
+ */
635
+ configure(config) {
636
+ this.config = config;
637
+ }
638
+ /**
639
+ * Join a voice channel
640
+ */
641
+ async join(channel, options = {}) {
642
+ const guildId = channel.guild.id;
643
+ const existing = getVoiceConnection(guildId);
644
+ if (existing) {
645
+ existing.destroy();
646
+ }
647
+ const connection = joinVoiceChannel({
648
+ channelId: channel.id,
649
+ guildId,
650
+ adapterCreator: channel.guild.voiceAdapterCreator,
651
+ selfDeaf: options.selfDeaf ?? this.config.connection?.self_deaf ?? true,
652
+ selfMute: options.selfMute ?? this.config.connection?.self_mute ?? false
653
+ });
654
+ await entersState(connection, VoiceConnectionStatus.Ready, 3e4);
655
+ const player = createAudioPlayer();
656
+ player.on(AudioPlayerStatus.Idle, () => {
657
+ this.handleTrackEnd(guildId);
658
+ });
659
+ player.on("error", (error) => {
660
+ console.error(`Audio player error in ${guildId}:`, error);
661
+ this.handleTrackEnd(guildId);
662
+ });
663
+ connection.subscribe(player);
664
+ this.guildStates.set(guildId, {
665
+ connection,
666
+ player,
667
+ queue: [],
668
+ currentTrack: null,
669
+ volume: this.config.default_volume ?? 100,
670
+ loopMode: this.config.default_loop ?? "off",
671
+ filters: /* @__PURE__ */ new Set(),
672
+ paused: false
673
+ });
674
+ return connection;
675
+ }
676
+ /**
677
+ * Leave a voice channel
678
+ */
679
+ leave(guildId) {
680
+ const state = this.guildStates.get(guildId);
681
+ if (!state) return false;
682
+ state.player.stop();
683
+ state.connection.destroy();
684
+ this.guildStates.delete(guildId);
685
+ return true;
686
+ }
687
+ /**
688
+ * Play audio from a URL or file
689
+ */
690
+ async play(guildId, source, options = {}) {
691
+ const state = this.guildStates.get(guildId);
692
+ if (!state) {
693
+ throw new Error("Not connected to voice in this guild");
694
+ }
695
+ const resource = createAudioResource(source, {
696
+ inlineVolume: true
697
+ });
698
+ const volume = options.volume ?? state.volume;
699
+ resource.volume?.setVolume(volume / 100);
700
+ state.player.play(resource);
701
+ state.paused = false;
702
+ }
703
+ /**
704
+ * Add a track to the queue
705
+ */
706
+ addToQueue(guildId, item, position) {
707
+ const state = this.guildStates.get(guildId);
708
+ if (!state) {
709
+ throw new Error("Not connected to voice in this guild");
710
+ }
711
+ const maxSize = this.config.max_queue_size ?? 1e3;
712
+ if (state.queue.length >= maxSize) {
713
+ throw new Error(`Queue is full (max ${maxSize} tracks)`);
714
+ }
715
+ if (position === "next" || position === 0) {
716
+ state.queue.unshift(item);
717
+ return 0;
718
+ } else if (position === "last" || position === void 0) {
719
+ state.queue.push(item);
720
+ return state.queue.length - 1;
721
+ } else if (typeof position === "number") {
722
+ state.queue.splice(position, 0, item);
723
+ return position;
724
+ }
725
+ state.queue.push(item);
726
+ return state.queue.length - 1;
727
+ }
728
+ /**
729
+ * Remove a track from the queue
730
+ */
731
+ removeFromQueue(guildId, position) {
732
+ const state = this.guildStates.get(guildId);
733
+ if (!state) return null;
734
+ const [removed] = state.queue.splice(position, 1);
735
+ return removed ?? null;
736
+ }
737
+ /**
738
+ * Clear the queue
739
+ */
740
+ clearQueue(guildId) {
741
+ const state = this.guildStates.get(guildId);
742
+ if (!state) return 0;
743
+ const count = state.queue.length;
744
+ state.queue = [];
745
+ return count;
746
+ }
747
+ /**
748
+ * Shuffle the queue
749
+ */
750
+ shuffleQueue(guildId) {
751
+ const state = this.guildStates.get(guildId);
752
+ if (!state) return;
753
+ for (let i = state.queue.length - 1; i > 0; i--) {
754
+ const j = Math.floor(Math.random() * (i + 1));
755
+ [state.queue[i], state.queue[j]] = [state.queue[j], state.queue[i]];
756
+ }
757
+ }
758
+ /**
759
+ * Skip the current track
760
+ */
761
+ skip(guildId) {
762
+ const state = this.guildStates.get(guildId);
763
+ if (!state) return false;
764
+ state.player.stop();
765
+ return true;
766
+ }
767
+ /**
768
+ * Pause playback
769
+ */
770
+ pause(guildId) {
771
+ const state = this.guildStates.get(guildId);
772
+ if (!state) return false;
773
+ state.player.pause();
774
+ state.paused = true;
775
+ return true;
776
+ }
777
+ /**
778
+ * Resume playback
779
+ */
780
+ resume(guildId) {
781
+ const state = this.guildStates.get(guildId);
782
+ if (!state) return false;
783
+ state.player.unpause();
784
+ state.paused = false;
785
+ return true;
786
+ }
787
+ /**
788
+ * Stop playback
789
+ */
790
+ stop(guildId) {
791
+ const state = this.guildStates.get(guildId);
792
+ if (!state) return false;
793
+ state.player.stop();
794
+ state.queue = [];
795
+ state.currentTrack = null;
796
+ return true;
797
+ }
798
+ /**
799
+ * Set volume
800
+ */
801
+ setVolume(guildId, volume) {
802
+ const state = this.guildStates.get(guildId);
803
+ if (!state) return false;
804
+ state.volume = Math.max(0, Math.min(200, volume));
805
+ return true;
806
+ }
807
+ /**
808
+ * Set loop mode
809
+ */
810
+ setLoopMode(guildId, mode) {
811
+ const state = this.guildStates.get(guildId);
812
+ if (!state) return;
813
+ state.loopMode = mode;
814
+ }
815
+ /**
816
+ * Get the current state for a guild
817
+ */
818
+ getState(guildId) {
819
+ return this.guildStates.get(guildId);
820
+ }
821
+ /**
822
+ * Check if connected to a guild
823
+ */
824
+ isConnected(guildId) {
825
+ return this.guildStates.has(guildId);
826
+ }
827
+ /**
828
+ * Get the queue for a guild
829
+ */
830
+ getQueue(guildId) {
831
+ return this.guildStates.get(guildId)?.queue ?? [];
832
+ }
833
+ /**
834
+ * Get the current track for a guild
835
+ */
836
+ getCurrentTrack(guildId) {
837
+ return this.guildStates.get(guildId)?.currentTrack ?? null;
838
+ }
839
+ /**
840
+ * Handle track end (play next or loop)
841
+ */
842
+ async handleTrackEnd(guildId) {
843
+ const state = this.guildStates.get(guildId);
844
+ if (!state) return;
845
+ if (state.loopMode === "track" && state.currentTrack) {
846
+ await this.play(guildId, state.currentTrack.url);
847
+ return;
848
+ }
849
+ if (state.loopMode === "queue" && state.currentTrack) {
850
+ state.queue.push(state.currentTrack);
851
+ }
852
+ const next = state.queue.shift();
853
+ if (next) {
854
+ state.currentTrack = next;
855
+ await this.play(guildId, next.url);
856
+ } else {
857
+ state.currentTrack = null;
858
+ }
859
+ }
860
+ /**
861
+ * Disconnect from all voice channels
862
+ */
863
+ disconnectAll() {
864
+ for (const [guildId] of this.guildStates) {
865
+ this.leave(guildId);
866
+ }
867
+ }
868
+ };
869
+ function createVoiceManager() {
870
+ return new VoiceManager();
871
+ }
872
+
873
+ // src/video/index.ts
874
+ var VideoManager = class {
875
+ client;
876
+ config = {};
877
+ streamingMembers = /* @__PURE__ */ new Map();
878
+ // guildId -> Map<memberId, StreamInfo>
879
+ listeners = [];
880
+ initialized = false;
881
+ constructor(client) {
882
+ this.client = client;
883
+ }
884
+ /**
885
+ * Configure and initialize the video manager
886
+ */
887
+ configure(config) {
888
+ this.config = config;
889
+ if (config.stream_detection && !this.initialized) {
890
+ this.setupListener();
891
+ this.initialized = true;
892
+ }
893
+ }
894
+ /**
895
+ * Set up voice state update listener for stream detection
896
+ */
897
+ setupListener() {
898
+ this.client.on("voiceStateUpdate", (oldState, newState) => {
899
+ this.handleVoiceStateUpdate(oldState, newState);
900
+ });
901
+ }
902
+ /**
903
+ * Handle voice state update for stream detection
904
+ */
905
+ async handleVoiceStateUpdate(oldState, newState) {
906
+ const guildId = newState.guild.id;
907
+ const memberId = newState.member?.id;
908
+ if (!memberId || !newState.member) return;
909
+ if (!this.streamingMembers.has(guildId)) {
910
+ this.streamingMembers.set(guildId, /* @__PURE__ */ new Map());
911
+ }
912
+ const streaming = this.streamingMembers.get(guildId);
913
+ const wasStreaming = oldState.streaming;
914
+ const isStreaming = newState.streaming;
915
+ if (!wasStreaming && isStreaming && newState.channelId) {
916
+ const streamInfo = {
917
+ memberId,
918
+ username: newState.member.user.username,
919
+ channelId: newState.channelId,
920
+ startedAt: /* @__PURE__ */ new Date()
921
+ };
922
+ streaming.set(memberId, streamInfo);
923
+ const event = {
924
+ type: "start",
925
+ member: newState.member,
926
+ channelId: newState.channelId,
927
+ guildId,
928
+ timestamp: /* @__PURE__ */ new Date()
929
+ };
930
+ await this.emit(event);
931
+ await this.sendNotification(event);
932
+ }
933
+ if (wasStreaming && !isStreaming) {
934
+ streaming.delete(memberId);
935
+ const event = {
936
+ type: "stop",
937
+ member: newState.member,
938
+ channelId: oldState.channelId ?? "",
939
+ guildId,
940
+ timestamp: /* @__PURE__ */ new Date()
941
+ };
942
+ await this.emit(event);
943
+ }
944
+ if (wasStreaming && !newState.channelId) {
945
+ streaming.delete(memberId);
946
+ const event = {
947
+ type: "stop",
948
+ member: newState.member,
949
+ channelId: oldState.channelId ?? "",
950
+ guildId,
951
+ timestamp: /* @__PURE__ */ new Date()
952
+ };
953
+ await this.emit(event);
954
+ }
955
+ }
956
+ /**
957
+ * Emit a stream event to all listeners
958
+ */
959
+ async emit(event) {
960
+ for (const listener of this.listeners) {
961
+ try {
962
+ await listener(event);
963
+ } catch (error) {
964
+ console.error("Stream event listener error:", error);
965
+ }
966
+ }
967
+ }
968
+ /**
969
+ * Send notification when a stream starts
970
+ */
971
+ async sendNotification(event) {
972
+ if (event.type !== "start" || !this.config.notify_channel) {
973
+ return;
974
+ }
975
+ try {
976
+ const guild = this.client.guilds.cache.get(event.guildId);
977
+ if (!guild) return;
978
+ const channelId = typeof this.config.notify_channel === "string" ? this.config.notify_channel.replace(/[<#>]/g, "") : String(this.config.notify_channel);
979
+ const channel = guild.channels.cache.get(channelId);
980
+ if (!channel || !channel.isTextBased()) return;
981
+ const voiceChannel = guild.channels.cache.get(event.channelId);
982
+ const voiceChannelName = voiceChannel?.name ?? "Unknown Channel";
983
+ let content = `**${event.member.displayName}** started streaming in **${voiceChannelName}**!`;
984
+ if (this.config.notify_role) {
985
+ const roleId = typeof this.config.notify_role === "string" ? this.config.notify_role.replace(/[<@&>]/g, "") : String(this.config.notify_role);
986
+ const role = guild.roles.cache.get(roleId);
987
+ if (role) {
988
+ content = `${role.toString()} ${content}`;
989
+ }
990
+ }
991
+ await channel.send({
992
+ content,
993
+ allowedMentions: {
994
+ roles: this.config.notify_role ? [String(this.config.notify_role).replace(/[<@&>]/g, "")] : []
995
+ }
996
+ });
997
+ } catch (error) {
998
+ console.error("Failed to send stream notification:", error);
999
+ }
1000
+ }
1001
+ /**
1002
+ * Register a stream event listener
1003
+ */
1004
+ onStreamEvent(callback) {
1005
+ this.listeners.push(callback);
1006
+ return () => {
1007
+ const index = this.listeners.indexOf(callback);
1008
+ if (index !== -1) {
1009
+ this.listeners.splice(index, 1);
1010
+ }
1011
+ };
1012
+ }
1013
+ /**
1014
+ * Get all members currently streaming in a guild
1015
+ */
1016
+ getStreamingMembers(guildId) {
1017
+ const streaming = this.streamingMembers.get(guildId);
1018
+ if (!streaming) return [];
1019
+ return [...streaming.values()];
1020
+ }
1021
+ /**
1022
+ * Get streaming info for a specific member
1023
+ */
1024
+ getStreamInfo(guildId, memberId) {
1025
+ return this.streamingMembers.get(guildId)?.get(memberId) ?? null;
1026
+ }
1027
+ /**
1028
+ * Check if a member is streaming
1029
+ */
1030
+ isStreaming(guildId, memberId) {
1031
+ return this.streamingMembers.get(guildId)?.has(memberId) ?? false;
1032
+ }
1033
+ /**
1034
+ * Get stream count in a guild
1035
+ */
1036
+ getStreamCount(guildId) {
1037
+ return this.streamingMembers.get(guildId)?.size ?? 0;
1038
+ }
1039
+ /**
1040
+ * Get total active streams across all guilds
1041
+ */
1042
+ getTotalStreamCount() {
1043
+ let count = 0;
1044
+ for (const streaming of this.streamingMembers.values()) {
1045
+ count += streaming.size;
1046
+ }
1047
+ return count;
1048
+ }
1049
+ /**
1050
+ * Get all active streams across all guilds
1051
+ */
1052
+ getAllActiveStreams() {
1053
+ const result = [];
1054
+ for (const [guildId, streaming] of this.streamingMembers) {
1055
+ if (streaming.size > 0) {
1056
+ result.push({ guildId, streams: [...streaming.values()] });
1057
+ }
1058
+ }
1059
+ return result;
1060
+ }
1061
+ /**
1062
+ * Check if stream detection is enabled
1063
+ */
1064
+ isEnabled() {
1065
+ return this.config.stream_detection ?? false;
1066
+ }
1067
+ /**
1068
+ * Get current configuration
1069
+ */
1070
+ getConfig() {
1071
+ return { ...this.config };
1072
+ }
1073
+ };
1074
+ function createVideoManager(client) {
1075
+ return new VideoManager(client);
1076
+ }
1077
+ export {
1078
+ FurlowClient,
1079
+ GatewayManager,
1080
+ InteractionHandler,
1081
+ VideoManager,
1082
+ VoiceManager,
1083
+ createClient,
1084
+ createGatewayManager,
1085
+ createInteractionHandler,
1086
+ createVideoManager,
1087
+ createVoiceManager
1088
+ };
1089
+ //# sourceMappingURL=index.js.map