@stoatx/client 0.7.0 → 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
 
@@ -1428,13 +1432,14 @@ var RESTManager = class {
1428
1432
  this.client.emit("debug", `Bucket [${method}:${endpoint}] exhausted. Waiting ${waitTime}ms proactively...`);
1429
1433
  await sleep(waitTime);
1430
1434
  }
1435
+ const isBodyMethod = ["post", "patch", "put"].includes(method.toLowerCase());
1431
1436
  const response = await fetch(url, {
1432
1437
  method: method.toUpperCase(),
1433
1438
  headers: {
1434
1439
  "X-Bot-Token": this.token,
1435
1440
  "Content-Type": "application/json"
1436
1441
  },
1437
- ...body !== void 0 ? { body: JSON.stringify(body) } : {}
1442
+ ...isBodyMethod ? { body: JSON.stringify(body ?? {}) } : {}
1438
1443
  });
1439
1444
  const remainingHeader = response.headers.get("x-ratelimit-remaining");
1440
1445
  const resetAfterHeader = response.headers.get("x-ratelimit-reset-after");
@@ -1970,6 +1975,9 @@ var BaseChannel = class extends Base {
1970
1975
  isGroup() {
1971
1976
  return this.type === "Group";
1972
1977
  }
1978
+ isVoice() {
1979
+ return this.isText() && this.voice !== null;
1980
+ }
1973
1981
  };
1974
1982
 
1975
1983
  // src/structures/TextChannel.ts
@@ -2280,10 +2288,21 @@ var GroupChannel = class extends BaseChannel {
2280
2288
  }
2281
2289
  };
2282
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
+
2283
2301
  // src/utils/ChannelFactory.ts
2284
2302
  function createChannel(client, data) {
2285
2303
  switch (data.channel_type) {
2286
2304
  case "TextChannel":
2305
+ if (data.voice) return new VoiceChannel(client, data);
2287
2306
  return new TextChannel(client, data);
2288
2307
  case "DirectMessage":
2289
2308
  return new DMChannel(client, data);
@@ -4429,6 +4448,237 @@ var SweeperManager = class {
4429
4448
  }
4430
4449
  };
4431
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
+
4432
4682
  // src/client/Client.ts
4433
4683
  var Client = class extends import_events2.EventEmitter {
4434
4684
  rest;
@@ -4440,6 +4690,7 @@ var Client = class extends import_events2.EventEmitter {
4440
4690
  emojis;
4441
4691
  user = null;
4442
4692
  options;
4693
+ voice;
4443
4694
  constructor(options = {}) {
4444
4695
  super({ captureRejections: true });
4445
4696
  this.options = options;
@@ -4449,6 +4700,7 @@ var Client = class extends import_events2.EventEmitter {
4449
4700
  this.servers = new ServerManager(this, options.cacheLimits?.servers);
4450
4701
  this.users = new UserManager(this, options.cacheLimits?.users);
4451
4702
  this.emojis = new EmojiManager(this, void 0, options.cacheLimits?.emojis);
4703
+ this.voice = new VoiceManager(this);
4452
4704
  this.sweepers = new SweeperManager(this, options.sweepers ?? {});
4453
4705
  }
4454
4706
  /**
@@ -4544,6 +4796,8 @@ var API = __toESM(require("stoat-api"), 1);
4544
4796
  API,
4545
4797
  Attachment,
4546
4798
  AttachmentBuilder,
4799
+ AudioPlayer,
4800
+ AudioResource,
4547
4801
  Base,
4548
4802
  BaseChannel,
4549
4803
  BitField,
@@ -4578,6 +4832,8 @@ var API = __toESM(require("stoat-api"), 1);
4578
4832
  TextChannel,
4579
4833
  UnknownChannel,
4580
4834
  User,
4581
- UserManager
4835
+ UserManager,
4836
+ VoiceConnection,
4837
+ VoiceManager
4582
4838
  });
4583
4839
  //# sourceMappingURL=index.cjs.map