@stoatx/client 0.6.4 → 0.8.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 CHANGED
@@ -33,6 +33,8 @@ __export(index_exports, {
33
33
  API: () => API,
34
34
  Attachment: () => Attachment,
35
35
  AttachmentBuilder: () => AttachmentBuilder,
36
+ AudioPlayer: () => AudioPlayer,
37
+ AudioResource: () => AudioResource,
36
38
  Base: () => Base,
37
39
  BaseChannel: () => BaseChannel,
38
40
  BitField: () => BitField,
@@ -67,7 +69,9 @@ __export(index_exports, {
67
69
  TextChannel: () => TextChannel,
68
70
  UnknownChannel: () => UnknownChannel,
69
71
  User: () => User,
70
- UserManager: () => UserManager
72
+ UserManager: () => UserManager,
73
+ VoiceConnection: () => VoiceConnection,
74
+ VoiceManager: () => VoiceManager
71
75
  });
72
76
  module.exports = __toCommonJS(index_exports);
73
77
 
@@ -829,6 +833,7 @@ var GatewayManager = class {
829
833
  this.client = client;
830
834
  }
831
835
  ws = null;
836
+ wsURL = "";
832
837
  pingInterval = null;
833
838
  token = null;
834
839
  reconnectAttempts = 0;
@@ -843,7 +848,7 @@ var GatewayManager = class {
843
848
  this.ws.removeAllListeners();
844
849
  this.ws = null;
845
850
  }
846
- const baseUrl = "wss://stoat.chat/events";
851
+ const baseUrl = this.wsURL ?? "wss://events.stoat.chat";
847
852
  const url = `${baseUrl}?version=1&format=json&token=${this.token}`;
848
853
  this.ws = new import_ws.default(url);
849
854
  this.ws.on("open", () => {
@@ -863,6 +868,9 @@ var GatewayManager = class {
863
868
  this.client.emit("error", error);
864
869
  });
865
870
  }
871
+ setGatewayUrl(url) {
872
+ this.wsURL = url;
873
+ }
866
874
  handleMessage(rawData) {
867
875
  const payload = JSON.parse(rawData.toString());
868
876
  const eventType = payload.type;
@@ -1359,12 +1367,19 @@ var RESTManager = class {
1359
1367
  }
1360
1368
  }, 3e5).unref();
1361
1369
  }
1362
- baseURL = "https://stoat.chat/api";
1370
+ baseURL = "";
1371
+ cdnURL = "";
1363
1372
  token = null;
1364
1373
  buckets = /* @__PURE__ */ new Map();
1365
1374
  setToken(token) {
1366
1375
  this.token = token;
1367
1376
  }
1377
+ setBaseURL(baseURL) {
1378
+ this.baseURL = baseURL.replace(/\/+$/, "");
1379
+ }
1380
+ setCDNURL(cdnURL) {
1381
+ this.cdnURL = cdnURL.replace(/\/+$/, "");
1382
+ }
1368
1383
  /**
1369
1384
  * Generates a local identifier for the bucket based on method and path.
1370
1385
  */
@@ -1417,13 +1432,14 @@ var RESTManager = class {
1417
1432
  this.client.emit("debug", `Bucket [${method}:${endpoint}] exhausted. Waiting ${waitTime}ms proactively...`);
1418
1433
  await sleep(waitTime);
1419
1434
  }
1435
+ const isBodyMethod = ["post", "patch", "put"].includes(method.toLowerCase());
1420
1436
  const response = await fetch(url, {
1421
1437
  method: method.toUpperCase(),
1422
1438
  headers: {
1423
1439
  "X-Bot-Token": this.token,
1424
1440
  "Content-Type": "application/json"
1425
1441
  },
1426
- ...body !== void 0 ? { body: JSON.stringify(body) } : {}
1442
+ ...isBodyMethod ? { body: JSON.stringify(body ?? {}) } : {}
1427
1443
  });
1428
1444
  const remainingHeader = response.headers.get("x-ratelimit-remaining");
1429
1445
  const resetAfterHeader = response.headers.get("x-ratelimit-reset-after");
@@ -1456,7 +1472,7 @@ var RESTManager = class {
1456
1472
  */
1457
1473
  async uploadFile(tag, fileBuffer, filename) {
1458
1474
  if (!this.token) throw new Error("REST_NOT_READY: No token available.");
1459
- const url = `https://cdn.stoatusercontent.com/${tag}`;
1475
+ const url = `${this.cdnURL}/${tag}`;
1460
1476
  const formData = new FormData();
1461
1477
  formData.append("file", new Blob([fileBuffer]), filename);
1462
1478
  const response = await fetch(url, {
@@ -1959,6 +1975,9 @@ var BaseChannel = class extends Base {
1959
1975
  isGroup() {
1960
1976
  return this.type === "Group";
1961
1977
  }
1978
+ isVoice() {
1979
+ return this.isText() && this.voice !== null;
1980
+ }
1962
1981
  };
1963
1982
 
1964
1983
  // src/structures/TextChannel.ts
@@ -2269,10 +2288,21 @@ var GroupChannel = class extends BaseChannel {
2269
2288
  }
2270
2289
  };
2271
2290
 
2291
+ // src/structures/VoiceChannel.ts
2292
+ var VoiceChannel = class extends TextChannel {
2293
+ async join() {
2294
+ return this.client.voice.join(this.id, this.serverId);
2295
+ }
2296
+ async leave() {
2297
+ return this.client.voice.leave(this.id);
2298
+ }
2299
+ };
2300
+
2272
2301
  // src/utils/ChannelFactory.ts
2273
2302
  function createChannel(client, data) {
2274
2303
  switch (data.channel_type) {
2275
2304
  case "TextChannel":
2305
+ if (data.voice) return new VoiceChannel(client, data);
2276
2306
  return new TextChannel(client, data);
2277
2307
  case "DirectMessage":
2278
2308
  return new DMChannel(client, data);
@@ -3852,6 +3882,50 @@ var Emoji = class extends Base {
3852
3882
  if (data.animated !== void 0) this.animated = data.animated;
3853
3883
  if (data.nsfw !== void 0) this.nsfw = data.nsfw;
3854
3884
  }
3885
+ /**
3886
+ * Fetch this emoji from the API or resolves it from the local cache.
3887
+ * @param force Whether to skip the cache check and force a direct API request. Defaults to false.
3888
+ * @returns A promise that resolves to the fetched {@link Emoji} object.
3889
+ * @throws {Error} If the API request fails or if the emoji is detached.
3890
+ * @example
3891
+ * // Force fetch emoji to update its data
3892
+ * await emoji.fetch(true);
3893
+ */
3894
+ async fetch(force) {
3895
+ const server = this.client.servers.cache.get(this.parent.type === "Server" ? this.parent.id : "");
3896
+ if (!server) throw new Error("No server registered in EmojiManager");
3897
+ if (this.parent.type === "Detached") throw new Error("Cannot fetch a detached emoji");
3898
+ return await server.emojis.fetch(this, force);
3899
+ }
3900
+ /**
3901
+ * Deletes this emoji from the server.
3902
+ * @returns A promise that resolves when the emoji has been deleted.
3903
+ * @throws {Error} If the API request fails or if the emoji is detached.
3904
+ * @example
3905
+ * // Delete an emoji from the server
3906
+ * await emoji.delete();
3907
+ */
3908
+ async delete() {
3909
+ const server = this.client.servers.cache.get(this.parent.type === "Server" ? this.parent.id : "");
3910
+ if (!server) throw new Error("No server registered in EmojiManager");
3911
+ if (this.parent.type === "Detached") throw new Error("Emoji is already detached");
3912
+ await server.emojis.delete(this);
3913
+ }
3914
+ /**
3915
+ * Edits this emoji's properties.
3916
+ * @param options The options to edit the emoji with.
3917
+ * @returns A promise that resolves to the edited {@link Emoji} object.
3918
+ * @throws {Error} If the API request fails or if the emoji is detached.
3919
+ * @example
3920
+ * // Edit an emoji's name
3921
+ * await emoji.edit({ name: "new_name" });
3922
+ */
3923
+ async edit(options) {
3924
+ const server = this.client.servers.cache.get(this.parent.type === "Server" ? this.parent.id : "");
3925
+ if (!server) throw new Error("No server registered in EmojiManager");
3926
+ if (this.parent.type === "Detached") throw new Error("Cannot edit a detached emoji");
3927
+ return await server.emojis.edit(this, options);
3928
+ }
3855
3929
  };
3856
3930
 
3857
3931
  // src/managers/EmojiManager.ts
@@ -3865,7 +3939,7 @@ var EmojiManager = class extends BaseManager {
3865
3939
  * Tell BaseManager how to find the ID for Emojis
3866
3940
  */
3867
3941
  extractId(data) {
3868
- return data._id ?? data.id;
3942
+ return data._id;
3869
3943
  }
3870
3944
  /**
3871
3945
  * Tell BaseManager how to build an Emoji
@@ -3873,10 +3947,104 @@ var EmojiManager = class extends BaseManager {
3873
3947
  construct(data) {
3874
3948
  return new Emoji(this.client, data);
3875
3949
  }
3876
- async fetch(id) {
3950
+ /**
3951
+ * Fetch an Emoji from the API or resolves it from the local cache.
3952
+ * @param emoji The ID, mention, or {@link Emoji} object to fetch
3953
+ * @param force Whether to skip the cache check and force a direct API request. Defaults to false.
3954
+ * @returns A promise that resolves to the fetched {@link Emoji} object.
3955
+ * @throws {TypeError} If an invalid {@link EmojiResolvable} is provided.
3956
+ * @throws {Error} If the API request fails.
3957
+ * @example
3958
+ * // Fetch a channel, bypassing cache
3959
+ * const channel = await client.channels.fetch("01H...", true);
3960
+ */
3961
+ async fetch(emoji, force = false) {
3962
+ if (!force) {
3963
+ const cached = this.resolve(emoji);
3964
+ if (cached) return cached;
3965
+ }
3966
+ const id = this.resolveId(emoji);
3877
3967
  const data = await this.client.rest.get(`/custom/emoji/${id}`);
3878
3968
  return this._add(data);
3879
3969
  }
3970
+ /**
3971
+ * Resolves a {@link EmojiResolvable} to a {@link Emoji} object from the cache.
3972
+ * @param emoji The {@link EmojiResolvable} to resolve.
3973
+ * @returns The resolved {@link Emoji} object, or undefined if not found.
3974
+ */
3975
+ resolve(emoji) {
3976
+ if (emoji instanceof Emoji) return emoji;
3977
+ if (typeof emoji === "string") {
3978
+ const id = emoji.replace(/[<:>]/g, "");
3979
+ return this.cache.get(id);
3980
+ }
3981
+ return void 0;
3982
+ }
3983
+ /**
3984
+ * Extracts ID from a {@link EmojiResolvable}.
3985
+ * @param emoji The {@link EmojiResolvable} to extract the ID from.
3986
+ * @returns The extracted {@link Emoji} ID.
3987
+ * @throws {TypeError} If an invalid type is provided.
3988
+ */
3989
+ resolveId(emoji) {
3990
+ if (emoji instanceof Emoji) return emoji.id;
3991
+ if (typeof emoji === "string") {
3992
+ return emoji.replace(/:/g, "");
3993
+ }
3994
+ throw new Error("Invalid EmojiResolvable");
3995
+ }
3996
+ /**
3997
+ * Create a new emoji
3998
+ * @param options The options for creating the emoji
3999
+ * @returns A promise that resolves to the created {@link Emoji} object.
4000
+ * @throws {Error} If no server is registered in the {@link EmojiManager} or if the emoji attachment cannot be resolved.
4001
+ * @example
4002
+ * const emoji = await client.emojis.create(server, { emoji: "path/to/emoji.png", name: "myEmoji" });
4003
+ */
4004
+ async crate(options) {
4005
+ if (!this.server) throw new Error("No server registered in EmojiManager");
4006
+ const resolvedId = await resolveAttachment(this.client.rest, options.emoji, "emojis");
4007
+ if (!resolvedId) throw new Error("Failed to resolve emoji attachment");
4008
+ const payload = {
4009
+ name: options.name,
4010
+ parent: {
4011
+ type: "Server",
4012
+ id: this.server.id
4013
+ }
4014
+ };
4015
+ if (options.nsfw) payload.nsfw = options.nsfw;
4016
+ const data = await this.client.rest.put(`/custom/emoji/${resolvedId}`, payload);
4017
+ return this._add(data);
4018
+ }
4019
+ /**
4020
+ * Delete an emoji
4021
+ * @param emoji The {@link EmojiResolvable} to delete
4022
+ * @throws {Error} If no server is registered in the {@link EmojiManager}.
4023
+ * @example
4024
+ * await client.emojis.delete(emoji);
4025
+ */
4026
+ async delete(emoji) {
4027
+ const id = this.resolveId(emoji);
4028
+ await this.client.rest.delete(`/custom/emoji/${id}`);
4029
+ this.cache.delete(id);
4030
+ }
4031
+ /**
4032
+ * Edit an emoji
4033
+ * @param emoji The {@link EmojiResolvable} to edit
4034
+ * @param options The options to edit the emoji with
4035
+ * @returns A promise that resolves to the edited {@link Emoji} object.
4036
+ * @throws {Error} If no server is registered in the {@link EmojiManager}.
4037
+ * @example
4038
+ * const editedEmoji = await client.emojis.edit(emoji, { name: "newName" });
4039
+ */
4040
+ async edit(emoji, options) {
4041
+ const id = this.resolveId(emoji);
4042
+ const payload = {
4043
+ name: options.name
4044
+ };
4045
+ const data = await this.client.rest.patch(`/custom/emoji/${id}`, payload);
4046
+ return this._add(data);
4047
+ }
3880
4048
  [util11.inspect.custom]() {
3881
4049
  return this.cache;
3882
4050
  }
@@ -4280,8 +4448,249 @@ var SweeperManager = class {
4280
4448
  }
4281
4449
  };
4282
4450
 
4451
+ // src/voice/VoiceConnection.ts
4452
+ var import_rtc_node = require("@livekit/rtc-node");
4453
+ var import_node_events = require("events");
4454
+ var SAMPLE_RATE = 48e3;
4455
+ var CHANNELS = 2;
4456
+ var SAMPLES_PER_FRAME = 960;
4457
+ var BYTES_PER_FRAME = SAMPLES_PER_FRAME * CHANNELS * 2;
4458
+ var VoiceConnection = class extends import_node_events.EventEmitter {
4459
+ channelId;
4460
+ guildId;
4461
+ room;
4462
+ _status = "connecting";
4463
+ _player = null;
4464
+ _audioSource = null;
4465
+ _audioTrack = null;
4466
+ constructor(channelId, guildId) {
4467
+ super();
4468
+ this.channelId = channelId;
4469
+ this.guildId = guildId;
4470
+ this.room = new import_rtc_node.Room();
4471
+ this.room.on(import_rtc_node.RoomEvent.Disconnected, () => {
4472
+ this._status = "disconnected";
4473
+ this._player?.removeSubscriber(this);
4474
+ this.emit("disconnect");
4475
+ });
4476
+ }
4477
+ get status() {
4478
+ return this._status;
4479
+ }
4480
+ get player() {
4481
+ return this._player;
4482
+ }
4483
+ /** @internal */
4484
+ async connect(url, token) {
4485
+ await this.room.connect(url, token);
4486
+ this._status = "ready";
4487
+ this.emit("ready");
4488
+ }
4489
+ subscribe(player) {
4490
+ if (this._player) this._player.removeSubscriber(this);
4491
+ this._player = player;
4492
+ player.addSubscriber(this);
4493
+ }
4494
+ unsubscribe() {
4495
+ this._player?.removeSubscriber(this);
4496
+ this._player = null;
4497
+ }
4498
+ async _feedStream(stream) {
4499
+ if (!this._audioSource) {
4500
+ this._audioSource = new import_rtc_node.AudioSource(SAMPLE_RATE, CHANNELS, SAMPLES_PER_FRAME);
4501
+ this._audioTrack = import_rtc_node.LocalAudioTrack.createAudioTrack("audio", this._audioSource);
4502
+ const options = new import_rtc_node.TrackPublishOptions();
4503
+ options.source = import_rtc_node.TrackSource.SOURCE_MICROPHONE;
4504
+ if (!this.room.localParticipant) {
4505
+ throw new Error("Not connected to a room");
4506
+ }
4507
+ await this.room.localParticipant.publishTrack(this._audioTrack, options);
4508
+ }
4509
+ let leftover = Buffer.alloc(0);
4510
+ for await (const chunk of stream) {
4511
+ const buf = Buffer.concat([leftover, chunk]);
4512
+ let offset = 0;
4513
+ while (offset + BYTES_PER_FRAME <= buf.length) {
4514
+ const slice = buf.subarray(offset, offset + BYTES_PER_FRAME);
4515
+ const tmp = slice.buffer.slice(slice.byteOffset, slice.byteOffset + slice.byteLength);
4516
+ const pcm = new Int16Array(tmp);
4517
+ await this._audioSource.captureFrame(new import_rtc_node.AudioFrame(pcm, SAMPLE_RATE, CHANNELS, SAMPLES_PER_FRAME));
4518
+ offset += BYTES_PER_FRAME;
4519
+ }
4520
+ leftover = buf.subarray(offset);
4521
+ }
4522
+ }
4523
+ async disconnect() {
4524
+ this._status = "disconnecting";
4525
+ this.unsubscribe();
4526
+ if (this._audioTrack) {
4527
+ await this._audioTrack.close();
4528
+ this._audioTrack = null;
4529
+ this._audioSource = null;
4530
+ }
4531
+ await this.room.disconnect();
4532
+ }
4533
+ };
4534
+
4535
+ // src/voice/VoiceManager.ts
4536
+ var VoiceManager = class {
4537
+ connections = /* @__PURE__ */ new Map();
4538
+ client;
4539
+ constructor(client) {
4540
+ this.client = client;
4541
+ }
4542
+ async join(channelId, guildId) {
4543
+ const existing = this.connections.get(channelId);
4544
+ if (existing?.status === "ready") return existing;
4545
+ const { token, url } = await this.client.rest.post(`/channels/${channelId}/join_call`);
4546
+ const connection = new VoiceConnection(channelId, guildId);
4547
+ this.connections.set(channelId, connection);
4548
+ connection.once("disconnect", () => this.connections.delete(channelId));
4549
+ await connection.connect(url, token);
4550
+ return connection;
4551
+ }
4552
+ get(channelId) {
4553
+ return this.connections.get(channelId);
4554
+ }
4555
+ async leave(channelId) {
4556
+ await this.connections.get(channelId)?.disconnect();
4557
+ }
4558
+ async leaveAll() {
4559
+ await Promise.all([...this.connections.keys()].map((id) => this.leave(id)));
4560
+ }
4561
+ };
4562
+
4563
+ // src/voice/AudioPlayer.ts
4564
+ var import_node_events2 = require("events");
4565
+ var AudioPlayer = class extends import_node_events2.EventEmitter {
4566
+ _status = "idle";
4567
+ _resource = null;
4568
+ /** Registered VoiceConnections subscribed to this player */
4569
+ subscribers = /* @__PURE__ */ new Set();
4570
+ get status() {
4571
+ return this._status;
4572
+ }
4573
+ get resource() {
4574
+ return this._resource;
4575
+ }
4576
+ play(resource) {
4577
+ this.transition("buffering");
4578
+ this._resource = resource;
4579
+ resource.stream.once("readable", () => {
4580
+ this.transition("playing");
4581
+ this.feed();
4582
+ });
4583
+ resource.stream.once("end", () => {
4584
+ this._resource = null;
4585
+ this.transition("idle");
4586
+ this.emit("idle");
4587
+ });
4588
+ resource.stream.once("error", (err) => {
4589
+ this._resource = null;
4590
+ this.transition("idle");
4591
+ this.emit("error", err);
4592
+ });
4593
+ }
4594
+ pause() {
4595
+ if (this._status !== "playing") return;
4596
+ this._resource?.stream.pause();
4597
+ this.transition("paused");
4598
+ }
4599
+ resume() {
4600
+ if (this._status !== "paused") return;
4601
+ this._resource?.stream.resume();
4602
+ this.transition("playing");
4603
+ }
4604
+ stop() {
4605
+ this._resource?.stream.destroy();
4606
+ this._resource = null;
4607
+ this.transition("stopped");
4608
+ this.transition("idle");
4609
+ this.emit("idle");
4610
+ }
4611
+ /** @internal */
4612
+ addSubscriber(conn) {
4613
+ this.subscribers.add(conn);
4614
+ }
4615
+ /** @internal */
4616
+ removeSubscriber(conn) {
4617
+ this.subscribers.delete(conn);
4618
+ }
4619
+ feed() {
4620
+ if (!this._resource) return;
4621
+ for (const conn of this.subscribers) {
4622
+ conn._feedStream(this._resource.stream);
4623
+ }
4624
+ }
4625
+ transition(next) {
4626
+ const prev = this._status;
4627
+ this._status = next;
4628
+ this.emit("stateChange", prev, next);
4629
+ }
4630
+ };
4631
+
4632
+ // src/voice/AudioResource.ts
4633
+ var import_node_stream = require("stream");
4634
+ var import_node_child_process = require("child_process");
4635
+ var import_node_stream2 = require("stream");
4636
+ var import_node_fs = require("fs");
4637
+ var AudioResource = class _AudioResource {
4638
+ stream;
4639
+ constructor(stream) {
4640
+ this.stream = stream;
4641
+ }
4642
+ static from(source, options = {}) {
4643
+ if (typeof source === "string" && !source.startsWith("http") && !(0, import_node_fs.existsSync)(source)) {
4644
+ throw new Error(`Audio file not found: ${source}`);
4645
+ }
4646
+ const { volume = 1, inputType } = options;
4647
+ const args = [
4648
+ ...inputType ? ["-f", inputType] : [],
4649
+ "-i",
4650
+ typeof source === "string" ? source : "pipe:0",
4651
+ "-af",
4652
+ `volume=${volume}`,
4653
+ "-ar",
4654
+ "48000",
4655
+ "-ac",
4656
+ "2",
4657
+ "-f",
4658
+ "s16le",
4659
+ "-acodec",
4660
+ "pcm_s16le",
4661
+ "pipe:1"
4662
+ ];
4663
+ const ffmpeg = (0, import_node_child_process.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "ignore"] });
4664
+ if (source instanceof import_node_stream.Readable) {
4665
+ source.pipe(ffmpeg.stdin);
4666
+ source.once("error", () => ffmpeg.kill());
4667
+ }
4668
+ ffmpeg.stdin?.on("error", () => {
4669
+ });
4670
+ const pass = new import_node_stream2.PassThrough();
4671
+ ffmpeg.stdout.pipe(pass);
4672
+ ffmpeg.once("error", (err) => pass.destroy(err));
4673
+ ffmpeg.once("close", (code) => {
4674
+ if (code !== 0 && code !== null) {
4675
+ pass.destroy(new Error(`ffmpeg exited with code ${code}`));
4676
+ }
4677
+ });
4678
+ return new _AudioResource(pass);
4679
+ }
4680
+ };
4681
+
4283
4682
  // src/client/Client.ts
4284
4683
  var Client = class extends import_events2.EventEmitter {
4684
+ rest;
4685
+ gateway;
4686
+ channels;
4687
+ servers;
4688
+ users;
4689
+ sweepers;
4690
+ emojis;
4691
+ user = null;
4692
+ options;
4693
+ voice;
4285
4694
  constructor(options = {}) {
4286
4695
  super({ captureRejections: true });
4287
4696
  this.options = options;
@@ -4291,25 +4700,43 @@ var Client = class extends import_events2.EventEmitter {
4291
4700
  this.servers = new ServerManager(this, options.cacheLimits?.servers);
4292
4701
  this.users = new UserManager(this, options.cacheLimits?.users);
4293
4702
  this.emojis = new EmojiManager(this, void 0, options.cacheLimits?.emojis);
4703
+ this.voice = new VoiceManager(this);
4294
4704
  this.sweepers = new SweeperManager(this, options.sweepers ?? {});
4295
4705
  }
4296
- rest;
4297
- gateway;
4298
- channels;
4299
- servers;
4300
- users;
4301
- sweepers;
4302
- emojis;
4303
- user = null;
4304
4706
  /**
4305
4707
  * Connects the bot to the Stoat Gateway
4306
4708
  */
4307
4709
  async login(token) {
4308
4710
  if (!token) throw new Error("A valid token must be provided.");
4309
- this.sweepers.start();
4711
+ const rootApiUrl = this.options.apiURL ?? "https://api.stoat.chat";
4712
+ this.rest.setBaseURL(rootApiUrl);
4310
4713
  this.rest.setToken(token);
4714
+ const configData = await this.fetchConfig(rootApiUrl);
4715
+ const finalWsUrl = this.options.overrides?.wsURL ?? configData.ws;
4716
+ if (!finalWsUrl) {
4717
+ throw new Error(
4718
+ `[Stoat Misconfiguration] The server at '${rootApiUrl}' is running, but it has no WebSocket URL configured. The server administrator needs to set their gateway config, or you must bypass it using 'options.overrides.wsURL'.`
4719
+ );
4720
+ }
4721
+ this.gateway.setGatewayUrl(finalWsUrl);
4722
+ const finalCdnUrl = this.options.overrides?.cdnURL ?? configData.features.autumn.url;
4723
+ if (!finalCdnUrl) {
4724
+ throw new Error(
4725
+ `[Stoat Misconfiguration] The server at '${rootApiUrl}' is running, but it has no CDN URL configured. The server administrator needs to set their cdn config, or you must bypass it using 'options.overrides.cdnURL'.`
4726
+ );
4727
+ }
4728
+ this.rest.setCDNURL(finalCdnUrl);
4729
+ this.sweepers.start();
4311
4730
  return this.gateway.connect(token);
4312
4731
  }
4732
+ async fetchConfig(baseURL) {
4733
+ try {
4734
+ const response = await fetch(baseURL);
4735
+ return await response.json();
4736
+ } catch (error) {
4737
+ throw new Error(`Failed to fetch ${baseURL}, make sure the instance is running.`);
4738
+ }
4739
+ }
4313
4740
  [/* @__PURE__ */ Symbol.for("nodejs.rejection")](error) {
4314
4741
  this.emit("error", error);
4315
4742
  }
@@ -4369,6 +4796,8 @@ var API = __toESM(require("stoat-api"), 1);
4369
4796
  API,
4370
4797
  Attachment,
4371
4798
  AttachmentBuilder,
4799
+ AudioPlayer,
4800
+ AudioResource,
4372
4801
  Base,
4373
4802
  BaseChannel,
4374
4803
  BitField,
@@ -4403,6 +4832,8 @@ var API = __toESM(require("stoat-api"), 1);
4403
4832
  TextChannel,
4404
4833
  UnknownChannel,
4405
4834
  User,
4406
- UserManager
4835
+ UserManager,
4836
+ VoiceConnection,
4837
+ VoiceManager
4407
4838
  });
4408
4839
  //# sourceMappingURL=index.cjs.map