@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 +259 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -1
- package/dist/index.d.ts +81 -1
- package/dist/index.js +264 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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
|
-
...
|
|
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
|