@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/README.md +3 -113
- package/dist/index.cjs +448 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +185 -5
- package/dist/index.d.ts +185 -5
- package/dist/index.js +453 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/client/Client.ts
|
|
2
|
-
import { EventEmitter as
|
|
2
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
3
3
|
|
|
4
4
|
// src/gateway/GatewayManager.ts
|
|
5
5
|
import WebSocket from "ws";
|
|
@@ -756,6 +756,7 @@ var GatewayManager = class {
|
|
|
756
756
|
this.client = client;
|
|
757
757
|
}
|
|
758
758
|
ws = null;
|
|
759
|
+
wsURL = "";
|
|
759
760
|
pingInterval = null;
|
|
760
761
|
token = null;
|
|
761
762
|
reconnectAttempts = 0;
|
|
@@ -770,7 +771,7 @@ var GatewayManager = class {
|
|
|
770
771
|
this.ws.removeAllListeners();
|
|
771
772
|
this.ws = null;
|
|
772
773
|
}
|
|
773
|
-
const baseUrl = "wss://stoat.chat
|
|
774
|
+
const baseUrl = this.wsURL ?? "wss://events.stoat.chat";
|
|
774
775
|
const url = `${baseUrl}?version=1&format=json&token=${this.token}`;
|
|
775
776
|
this.ws = new WebSocket(url);
|
|
776
777
|
this.ws.on("open", () => {
|
|
@@ -790,6 +791,9 @@ var GatewayManager = class {
|
|
|
790
791
|
this.client.emit("error", error);
|
|
791
792
|
});
|
|
792
793
|
}
|
|
794
|
+
setGatewayUrl(url) {
|
|
795
|
+
this.wsURL = url;
|
|
796
|
+
}
|
|
793
797
|
handleMessage(rawData) {
|
|
794
798
|
const payload = JSON.parse(rawData.toString());
|
|
795
799
|
const eventType = payload.type;
|
|
@@ -1286,12 +1290,19 @@ var RESTManager = class {
|
|
|
1286
1290
|
}
|
|
1287
1291
|
}, 3e5).unref();
|
|
1288
1292
|
}
|
|
1289
|
-
baseURL = "
|
|
1293
|
+
baseURL = "";
|
|
1294
|
+
cdnURL = "";
|
|
1290
1295
|
token = null;
|
|
1291
1296
|
buckets = /* @__PURE__ */ new Map();
|
|
1292
1297
|
setToken(token) {
|
|
1293
1298
|
this.token = token;
|
|
1294
1299
|
}
|
|
1300
|
+
setBaseURL(baseURL) {
|
|
1301
|
+
this.baseURL = baseURL.replace(/\/+$/, "");
|
|
1302
|
+
}
|
|
1303
|
+
setCDNURL(cdnURL) {
|
|
1304
|
+
this.cdnURL = cdnURL.replace(/\/+$/, "");
|
|
1305
|
+
}
|
|
1295
1306
|
/**
|
|
1296
1307
|
* Generates a local identifier for the bucket based on method and path.
|
|
1297
1308
|
*/
|
|
@@ -1344,13 +1355,14 @@ var RESTManager = class {
|
|
|
1344
1355
|
this.client.emit("debug", `Bucket [${method}:${endpoint}] exhausted. Waiting ${waitTime}ms proactively...`);
|
|
1345
1356
|
await sleep(waitTime);
|
|
1346
1357
|
}
|
|
1358
|
+
const isBodyMethod = ["post", "patch", "put"].includes(method.toLowerCase());
|
|
1347
1359
|
const response = await fetch(url, {
|
|
1348
1360
|
method: method.toUpperCase(),
|
|
1349
1361
|
headers: {
|
|
1350
1362
|
"X-Bot-Token": this.token,
|
|
1351
1363
|
"Content-Type": "application/json"
|
|
1352
1364
|
},
|
|
1353
|
-
...
|
|
1365
|
+
...isBodyMethod ? { body: JSON.stringify(body ?? {}) } : {}
|
|
1354
1366
|
});
|
|
1355
1367
|
const remainingHeader = response.headers.get("x-ratelimit-remaining");
|
|
1356
1368
|
const resetAfterHeader = response.headers.get("x-ratelimit-reset-after");
|
|
@@ -1383,7 +1395,7 @@ var RESTManager = class {
|
|
|
1383
1395
|
*/
|
|
1384
1396
|
async uploadFile(tag, fileBuffer, filename) {
|
|
1385
1397
|
if (!this.token) throw new Error("REST_NOT_READY: No token available.");
|
|
1386
|
-
const url =
|
|
1398
|
+
const url = `${this.cdnURL}/${tag}`;
|
|
1387
1399
|
const formData = new FormData();
|
|
1388
1400
|
formData.append("file", new Blob([fileBuffer]), filename);
|
|
1389
1401
|
const response = await fetch(url, {
|
|
@@ -1886,6 +1898,9 @@ var BaseChannel = class extends Base {
|
|
|
1886
1898
|
isGroup() {
|
|
1887
1899
|
return this.type === "Group";
|
|
1888
1900
|
}
|
|
1901
|
+
isVoice() {
|
|
1902
|
+
return this.isText() && this.voice !== null;
|
|
1903
|
+
}
|
|
1889
1904
|
};
|
|
1890
1905
|
|
|
1891
1906
|
// src/structures/TextChannel.ts
|
|
@@ -2196,10 +2211,21 @@ var GroupChannel = class extends BaseChannel {
|
|
|
2196
2211
|
}
|
|
2197
2212
|
};
|
|
2198
2213
|
|
|
2214
|
+
// src/structures/VoiceChannel.ts
|
|
2215
|
+
var VoiceChannel = class extends TextChannel {
|
|
2216
|
+
async join() {
|
|
2217
|
+
return this.client.voice.join(this.id, this.serverId);
|
|
2218
|
+
}
|
|
2219
|
+
async leave() {
|
|
2220
|
+
return this.client.voice.leave(this.id);
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
|
|
2199
2224
|
// src/utils/ChannelFactory.ts
|
|
2200
2225
|
function createChannel(client, data) {
|
|
2201
2226
|
switch (data.channel_type) {
|
|
2202
2227
|
case "TextChannel":
|
|
2228
|
+
if (data.voice) return new VoiceChannel(client, data);
|
|
2203
2229
|
return new TextChannel(client, data);
|
|
2204
2230
|
case "DirectMessage":
|
|
2205
2231
|
return new DMChannel(client, data);
|
|
@@ -3779,6 +3805,50 @@ var Emoji = class extends Base {
|
|
|
3779
3805
|
if (data.animated !== void 0) this.animated = data.animated;
|
|
3780
3806
|
if (data.nsfw !== void 0) this.nsfw = data.nsfw;
|
|
3781
3807
|
}
|
|
3808
|
+
/**
|
|
3809
|
+
* Fetch this emoji from the API or resolves it from the local cache.
|
|
3810
|
+
* @param force Whether to skip the cache check and force a direct API request. Defaults to false.
|
|
3811
|
+
* @returns A promise that resolves to the fetched {@link Emoji} object.
|
|
3812
|
+
* @throws {Error} If the API request fails or if the emoji is detached.
|
|
3813
|
+
* @example
|
|
3814
|
+
* // Force fetch emoji to update its data
|
|
3815
|
+
* await emoji.fetch(true);
|
|
3816
|
+
*/
|
|
3817
|
+
async fetch(force) {
|
|
3818
|
+
const server = this.client.servers.cache.get(this.parent.type === "Server" ? this.parent.id : "");
|
|
3819
|
+
if (!server) throw new Error("No server registered in EmojiManager");
|
|
3820
|
+
if (this.parent.type === "Detached") throw new Error("Cannot fetch a detached emoji");
|
|
3821
|
+
return await server.emojis.fetch(this, force);
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* Deletes this emoji from the server.
|
|
3825
|
+
* @returns A promise that resolves when the emoji has been deleted.
|
|
3826
|
+
* @throws {Error} If the API request fails or if the emoji is detached.
|
|
3827
|
+
* @example
|
|
3828
|
+
* // Delete an emoji from the server
|
|
3829
|
+
* await emoji.delete();
|
|
3830
|
+
*/
|
|
3831
|
+
async delete() {
|
|
3832
|
+
const server = this.client.servers.cache.get(this.parent.type === "Server" ? this.parent.id : "");
|
|
3833
|
+
if (!server) throw new Error("No server registered in EmojiManager");
|
|
3834
|
+
if (this.parent.type === "Detached") throw new Error("Emoji is already detached");
|
|
3835
|
+
await server.emojis.delete(this);
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Edits this emoji's properties.
|
|
3839
|
+
* @param options The options to edit the emoji with.
|
|
3840
|
+
* @returns A promise that resolves to the edited {@link Emoji} object.
|
|
3841
|
+
* @throws {Error} If the API request fails or if the emoji is detached.
|
|
3842
|
+
* @example
|
|
3843
|
+
* // Edit an emoji's name
|
|
3844
|
+
* await emoji.edit({ name: "new_name" });
|
|
3845
|
+
*/
|
|
3846
|
+
async edit(options) {
|
|
3847
|
+
const server = this.client.servers.cache.get(this.parent.type === "Server" ? this.parent.id : "");
|
|
3848
|
+
if (!server) throw new Error("No server registered in EmojiManager");
|
|
3849
|
+
if (this.parent.type === "Detached") throw new Error("Cannot edit a detached emoji");
|
|
3850
|
+
return await server.emojis.edit(this, options);
|
|
3851
|
+
}
|
|
3782
3852
|
};
|
|
3783
3853
|
|
|
3784
3854
|
// src/managers/EmojiManager.ts
|
|
@@ -3792,7 +3862,7 @@ var EmojiManager = class extends BaseManager {
|
|
|
3792
3862
|
* Tell BaseManager how to find the ID for Emojis
|
|
3793
3863
|
*/
|
|
3794
3864
|
extractId(data) {
|
|
3795
|
-
return data._id
|
|
3865
|
+
return data._id;
|
|
3796
3866
|
}
|
|
3797
3867
|
/**
|
|
3798
3868
|
* Tell BaseManager how to build an Emoji
|
|
@@ -3800,10 +3870,104 @@ var EmojiManager = class extends BaseManager {
|
|
|
3800
3870
|
construct(data) {
|
|
3801
3871
|
return new Emoji(this.client, data);
|
|
3802
3872
|
}
|
|
3803
|
-
|
|
3873
|
+
/**
|
|
3874
|
+
* Fetch an Emoji from the API or resolves it from the local cache.
|
|
3875
|
+
* @param emoji The ID, mention, or {@link Emoji} object to fetch
|
|
3876
|
+
* @param force Whether to skip the cache check and force a direct API request. Defaults to false.
|
|
3877
|
+
* @returns A promise that resolves to the fetched {@link Emoji} object.
|
|
3878
|
+
* @throws {TypeError} If an invalid {@link EmojiResolvable} is provided.
|
|
3879
|
+
* @throws {Error} If the API request fails.
|
|
3880
|
+
* @example
|
|
3881
|
+
* // Fetch a channel, bypassing cache
|
|
3882
|
+
* const channel = await client.channels.fetch("01H...", true);
|
|
3883
|
+
*/
|
|
3884
|
+
async fetch(emoji, force = false) {
|
|
3885
|
+
if (!force) {
|
|
3886
|
+
const cached = this.resolve(emoji);
|
|
3887
|
+
if (cached) return cached;
|
|
3888
|
+
}
|
|
3889
|
+
const id = this.resolveId(emoji);
|
|
3804
3890
|
const data = await this.client.rest.get(`/custom/emoji/${id}`);
|
|
3805
3891
|
return this._add(data);
|
|
3806
3892
|
}
|
|
3893
|
+
/**
|
|
3894
|
+
* Resolves a {@link EmojiResolvable} to a {@link Emoji} object from the cache.
|
|
3895
|
+
* @param emoji The {@link EmojiResolvable} to resolve.
|
|
3896
|
+
* @returns The resolved {@link Emoji} object, or undefined if not found.
|
|
3897
|
+
*/
|
|
3898
|
+
resolve(emoji) {
|
|
3899
|
+
if (emoji instanceof Emoji) return emoji;
|
|
3900
|
+
if (typeof emoji === "string") {
|
|
3901
|
+
const id = emoji.replace(/[<:>]/g, "");
|
|
3902
|
+
return this.cache.get(id);
|
|
3903
|
+
}
|
|
3904
|
+
return void 0;
|
|
3905
|
+
}
|
|
3906
|
+
/**
|
|
3907
|
+
* Extracts ID from a {@link EmojiResolvable}.
|
|
3908
|
+
* @param emoji The {@link EmojiResolvable} to extract the ID from.
|
|
3909
|
+
* @returns The extracted {@link Emoji} ID.
|
|
3910
|
+
* @throws {TypeError} If an invalid type is provided.
|
|
3911
|
+
*/
|
|
3912
|
+
resolveId(emoji) {
|
|
3913
|
+
if (emoji instanceof Emoji) return emoji.id;
|
|
3914
|
+
if (typeof emoji === "string") {
|
|
3915
|
+
return emoji.replace(/:/g, "");
|
|
3916
|
+
}
|
|
3917
|
+
throw new Error("Invalid EmojiResolvable");
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* Create a new emoji
|
|
3921
|
+
* @param options The options for creating the emoji
|
|
3922
|
+
* @returns A promise that resolves to the created {@link Emoji} object.
|
|
3923
|
+
* @throws {Error} If no server is registered in the {@link EmojiManager} or if the emoji attachment cannot be resolved.
|
|
3924
|
+
* @example
|
|
3925
|
+
* const emoji = await client.emojis.create(server, { emoji: "path/to/emoji.png", name: "myEmoji" });
|
|
3926
|
+
*/
|
|
3927
|
+
async crate(options) {
|
|
3928
|
+
if (!this.server) throw new Error("No server registered in EmojiManager");
|
|
3929
|
+
const resolvedId = await resolveAttachment(this.client.rest, options.emoji, "emojis");
|
|
3930
|
+
if (!resolvedId) throw new Error("Failed to resolve emoji attachment");
|
|
3931
|
+
const payload = {
|
|
3932
|
+
name: options.name,
|
|
3933
|
+
parent: {
|
|
3934
|
+
type: "Server",
|
|
3935
|
+
id: this.server.id
|
|
3936
|
+
}
|
|
3937
|
+
};
|
|
3938
|
+
if (options.nsfw) payload.nsfw = options.nsfw;
|
|
3939
|
+
const data = await this.client.rest.put(`/custom/emoji/${resolvedId}`, payload);
|
|
3940
|
+
return this._add(data);
|
|
3941
|
+
}
|
|
3942
|
+
/**
|
|
3943
|
+
* Delete an emoji
|
|
3944
|
+
* @param emoji The {@link EmojiResolvable} to delete
|
|
3945
|
+
* @throws {Error} If no server is registered in the {@link EmojiManager}.
|
|
3946
|
+
* @example
|
|
3947
|
+
* await client.emojis.delete(emoji);
|
|
3948
|
+
*/
|
|
3949
|
+
async delete(emoji) {
|
|
3950
|
+
const id = this.resolveId(emoji);
|
|
3951
|
+
await this.client.rest.delete(`/custom/emoji/${id}`);
|
|
3952
|
+
this.cache.delete(id);
|
|
3953
|
+
}
|
|
3954
|
+
/**
|
|
3955
|
+
* Edit an emoji
|
|
3956
|
+
* @param emoji The {@link EmojiResolvable} to edit
|
|
3957
|
+
* @param options The options to edit the emoji with
|
|
3958
|
+
* @returns A promise that resolves to the edited {@link Emoji} object.
|
|
3959
|
+
* @throws {Error} If no server is registered in the {@link EmojiManager}.
|
|
3960
|
+
* @example
|
|
3961
|
+
* const editedEmoji = await client.emojis.edit(emoji, { name: "newName" });
|
|
3962
|
+
*/
|
|
3963
|
+
async edit(emoji, options) {
|
|
3964
|
+
const id = this.resolveId(emoji);
|
|
3965
|
+
const payload = {
|
|
3966
|
+
name: options.name
|
|
3967
|
+
};
|
|
3968
|
+
const data = await this.client.rest.patch(`/custom/emoji/${id}`, payload);
|
|
3969
|
+
return this._add(data);
|
|
3970
|
+
}
|
|
3807
3971
|
[util11.inspect.custom]() {
|
|
3808
3972
|
return this.cache;
|
|
3809
3973
|
}
|
|
@@ -4207,8 +4371,257 @@ var SweeperManager = class {
|
|
|
4207
4371
|
}
|
|
4208
4372
|
};
|
|
4209
4373
|
|
|
4374
|
+
// src/voice/VoiceConnection.ts
|
|
4375
|
+
import {
|
|
4376
|
+
Room,
|
|
4377
|
+
RoomEvent,
|
|
4378
|
+
TrackSource,
|
|
4379
|
+
LocalAudioTrack,
|
|
4380
|
+
TrackPublishOptions,
|
|
4381
|
+
AudioFrame,
|
|
4382
|
+
AudioSource
|
|
4383
|
+
} from "@livekit/rtc-node";
|
|
4384
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
4385
|
+
var SAMPLE_RATE = 48e3;
|
|
4386
|
+
var CHANNELS = 2;
|
|
4387
|
+
var SAMPLES_PER_FRAME = 960;
|
|
4388
|
+
var BYTES_PER_FRAME = SAMPLES_PER_FRAME * CHANNELS * 2;
|
|
4389
|
+
var VoiceConnection = class extends EventEmitter2 {
|
|
4390
|
+
channelId;
|
|
4391
|
+
guildId;
|
|
4392
|
+
room;
|
|
4393
|
+
_status = "connecting";
|
|
4394
|
+
_player = null;
|
|
4395
|
+
_audioSource = null;
|
|
4396
|
+
_audioTrack = null;
|
|
4397
|
+
constructor(channelId, guildId) {
|
|
4398
|
+
super();
|
|
4399
|
+
this.channelId = channelId;
|
|
4400
|
+
this.guildId = guildId;
|
|
4401
|
+
this.room = new Room();
|
|
4402
|
+
this.room.on(RoomEvent.Disconnected, () => {
|
|
4403
|
+
this._status = "disconnected";
|
|
4404
|
+
this._player?.removeSubscriber(this);
|
|
4405
|
+
this.emit("disconnect");
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
get status() {
|
|
4409
|
+
return this._status;
|
|
4410
|
+
}
|
|
4411
|
+
get player() {
|
|
4412
|
+
return this._player;
|
|
4413
|
+
}
|
|
4414
|
+
/** @internal */
|
|
4415
|
+
async connect(url, token) {
|
|
4416
|
+
await this.room.connect(url, token);
|
|
4417
|
+
this._status = "ready";
|
|
4418
|
+
this.emit("ready");
|
|
4419
|
+
}
|
|
4420
|
+
subscribe(player) {
|
|
4421
|
+
if (this._player) this._player.removeSubscriber(this);
|
|
4422
|
+
this._player = player;
|
|
4423
|
+
player.addSubscriber(this);
|
|
4424
|
+
}
|
|
4425
|
+
unsubscribe() {
|
|
4426
|
+
this._player?.removeSubscriber(this);
|
|
4427
|
+
this._player = null;
|
|
4428
|
+
}
|
|
4429
|
+
async _feedStream(stream) {
|
|
4430
|
+
if (!this._audioSource) {
|
|
4431
|
+
this._audioSource = new AudioSource(SAMPLE_RATE, CHANNELS, SAMPLES_PER_FRAME);
|
|
4432
|
+
this._audioTrack = LocalAudioTrack.createAudioTrack("audio", this._audioSource);
|
|
4433
|
+
const options = new TrackPublishOptions();
|
|
4434
|
+
options.source = TrackSource.SOURCE_MICROPHONE;
|
|
4435
|
+
if (!this.room.localParticipant) {
|
|
4436
|
+
throw new Error("Not connected to a room");
|
|
4437
|
+
}
|
|
4438
|
+
await this.room.localParticipant.publishTrack(this._audioTrack, options);
|
|
4439
|
+
}
|
|
4440
|
+
let leftover = Buffer.alloc(0);
|
|
4441
|
+
for await (const chunk of stream) {
|
|
4442
|
+
const buf = Buffer.concat([leftover, chunk]);
|
|
4443
|
+
let offset = 0;
|
|
4444
|
+
while (offset + BYTES_PER_FRAME <= buf.length) {
|
|
4445
|
+
const slice = buf.subarray(offset, offset + BYTES_PER_FRAME);
|
|
4446
|
+
const tmp = slice.buffer.slice(slice.byteOffset, slice.byteOffset + slice.byteLength);
|
|
4447
|
+
const pcm = new Int16Array(tmp);
|
|
4448
|
+
await this._audioSource.captureFrame(new AudioFrame(pcm, SAMPLE_RATE, CHANNELS, SAMPLES_PER_FRAME));
|
|
4449
|
+
offset += BYTES_PER_FRAME;
|
|
4450
|
+
}
|
|
4451
|
+
leftover = buf.subarray(offset);
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
async disconnect() {
|
|
4455
|
+
this._status = "disconnecting";
|
|
4456
|
+
this.unsubscribe();
|
|
4457
|
+
if (this._audioTrack) {
|
|
4458
|
+
await this._audioTrack.close();
|
|
4459
|
+
this._audioTrack = null;
|
|
4460
|
+
this._audioSource = null;
|
|
4461
|
+
}
|
|
4462
|
+
await this.room.disconnect();
|
|
4463
|
+
}
|
|
4464
|
+
};
|
|
4465
|
+
|
|
4466
|
+
// src/voice/VoiceManager.ts
|
|
4467
|
+
var VoiceManager = class {
|
|
4468
|
+
connections = /* @__PURE__ */ new Map();
|
|
4469
|
+
client;
|
|
4470
|
+
constructor(client) {
|
|
4471
|
+
this.client = client;
|
|
4472
|
+
}
|
|
4473
|
+
async join(channelId, guildId) {
|
|
4474
|
+
const existing = this.connections.get(channelId);
|
|
4475
|
+
if (existing?.status === "ready") return existing;
|
|
4476
|
+
const { token, url } = await this.client.rest.post(`/channels/${channelId}/join_call`);
|
|
4477
|
+
const connection = new VoiceConnection(channelId, guildId);
|
|
4478
|
+
this.connections.set(channelId, connection);
|
|
4479
|
+
connection.once("disconnect", () => this.connections.delete(channelId));
|
|
4480
|
+
await connection.connect(url, token);
|
|
4481
|
+
return connection;
|
|
4482
|
+
}
|
|
4483
|
+
get(channelId) {
|
|
4484
|
+
return this.connections.get(channelId);
|
|
4485
|
+
}
|
|
4486
|
+
async leave(channelId) {
|
|
4487
|
+
await this.connections.get(channelId)?.disconnect();
|
|
4488
|
+
}
|
|
4489
|
+
async leaveAll() {
|
|
4490
|
+
await Promise.all([...this.connections.keys()].map((id) => this.leave(id)));
|
|
4491
|
+
}
|
|
4492
|
+
};
|
|
4493
|
+
|
|
4494
|
+
// src/voice/AudioPlayer.ts
|
|
4495
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
4496
|
+
var AudioPlayer = class extends EventEmitter3 {
|
|
4497
|
+
_status = "idle";
|
|
4498
|
+
_resource = null;
|
|
4499
|
+
/** Registered VoiceConnections subscribed to this player */
|
|
4500
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
4501
|
+
get status() {
|
|
4502
|
+
return this._status;
|
|
4503
|
+
}
|
|
4504
|
+
get resource() {
|
|
4505
|
+
return this._resource;
|
|
4506
|
+
}
|
|
4507
|
+
play(resource) {
|
|
4508
|
+
this.transition("buffering");
|
|
4509
|
+
this._resource = resource;
|
|
4510
|
+
resource.stream.once("readable", () => {
|
|
4511
|
+
this.transition("playing");
|
|
4512
|
+
this.feed();
|
|
4513
|
+
});
|
|
4514
|
+
resource.stream.once("end", () => {
|
|
4515
|
+
this._resource = null;
|
|
4516
|
+
this.transition("idle");
|
|
4517
|
+
this.emit("idle");
|
|
4518
|
+
});
|
|
4519
|
+
resource.stream.once("error", (err) => {
|
|
4520
|
+
this._resource = null;
|
|
4521
|
+
this.transition("idle");
|
|
4522
|
+
this.emit("error", err);
|
|
4523
|
+
});
|
|
4524
|
+
}
|
|
4525
|
+
pause() {
|
|
4526
|
+
if (this._status !== "playing") return;
|
|
4527
|
+
this._resource?.stream.pause();
|
|
4528
|
+
this.transition("paused");
|
|
4529
|
+
}
|
|
4530
|
+
resume() {
|
|
4531
|
+
if (this._status !== "paused") return;
|
|
4532
|
+
this._resource?.stream.resume();
|
|
4533
|
+
this.transition("playing");
|
|
4534
|
+
}
|
|
4535
|
+
stop() {
|
|
4536
|
+
this._resource?.stream.destroy();
|
|
4537
|
+
this._resource = null;
|
|
4538
|
+
this.transition("stopped");
|
|
4539
|
+
this.transition("idle");
|
|
4540
|
+
this.emit("idle");
|
|
4541
|
+
}
|
|
4542
|
+
/** @internal */
|
|
4543
|
+
addSubscriber(conn) {
|
|
4544
|
+
this.subscribers.add(conn);
|
|
4545
|
+
}
|
|
4546
|
+
/** @internal */
|
|
4547
|
+
removeSubscriber(conn) {
|
|
4548
|
+
this.subscribers.delete(conn);
|
|
4549
|
+
}
|
|
4550
|
+
feed() {
|
|
4551
|
+
if (!this._resource) return;
|
|
4552
|
+
for (const conn of this.subscribers) {
|
|
4553
|
+
conn._feedStream(this._resource.stream);
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
transition(next) {
|
|
4557
|
+
const prev = this._status;
|
|
4558
|
+
this._status = next;
|
|
4559
|
+
this.emit("stateChange", prev, next);
|
|
4560
|
+
}
|
|
4561
|
+
};
|
|
4562
|
+
|
|
4563
|
+
// src/voice/AudioResource.ts
|
|
4564
|
+
import { Readable } from "stream";
|
|
4565
|
+
import { spawn } from "child_process";
|
|
4566
|
+
import { PassThrough } from "stream";
|
|
4567
|
+
import { existsSync } from "fs";
|
|
4568
|
+
var AudioResource = class _AudioResource {
|
|
4569
|
+
stream;
|
|
4570
|
+
constructor(stream) {
|
|
4571
|
+
this.stream = stream;
|
|
4572
|
+
}
|
|
4573
|
+
static from(source, options = {}) {
|
|
4574
|
+
if (typeof source === "string" && !source.startsWith("http") && !existsSync(source)) {
|
|
4575
|
+
throw new Error(`Audio file not found: ${source}`);
|
|
4576
|
+
}
|
|
4577
|
+
const { volume = 1, inputType } = options;
|
|
4578
|
+
const args = [
|
|
4579
|
+
...inputType ? ["-f", inputType] : [],
|
|
4580
|
+
"-i",
|
|
4581
|
+
typeof source === "string" ? source : "pipe:0",
|
|
4582
|
+
"-af",
|
|
4583
|
+
`volume=${volume}`,
|
|
4584
|
+
"-ar",
|
|
4585
|
+
"48000",
|
|
4586
|
+
"-ac",
|
|
4587
|
+
"2",
|
|
4588
|
+
"-f",
|
|
4589
|
+
"s16le",
|
|
4590
|
+
"-acodec",
|
|
4591
|
+
"pcm_s16le",
|
|
4592
|
+
"pipe:1"
|
|
4593
|
+
];
|
|
4594
|
+
const ffmpeg = spawn("ffmpeg", args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
4595
|
+
if (source instanceof Readable) {
|
|
4596
|
+
source.pipe(ffmpeg.stdin);
|
|
4597
|
+
source.once("error", () => ffmpeg.kill());
|
|
4598
|
+
}
|
|
4599
|
+
ffmpeg.stdin?.on("error", () => {
|
|
4600
|
+
});
|
|
4601
|
+
const pass = new PassThrough();
|
|
4602
|
+
ffmpeg.stdout.pipe(pass);
|
|
4603
|
+
ffmpeg.once("error", (err) => pass.destroy(err));
|
|
4604
|
+
ffmpeg.once("close", (code) => {
|
|
4605
|
+
if (code !== 0 && code !== null) {
|
|
4606
|
+
pass.destroy(new Error(`ffmpeg exited with code ${code}`));
|
|
4607
|
+
}
|
|
4608
|
+
});
|
|
4609
|
+
return new _AudioResource(pass);
|
|
4610
|
+
}
|
|
4611
|
+
};
|
|
4612
|
+
|
|
4210
4613
|
// src/client/Client.ts
|
|
4211
|
-
var Client = class extends
|
|
4614
|
+
var Client = class extends EventEmitter4 {
|
|
4615
|
+
rest;
|
|
4616
|
+
gateway;
|
|
4617
|
+
channels;
|
|
4618
|
+
servers;
|
|
4619
|
+
users;
|
|
4620
|
+
sweepers;
|
|
4621
|
+
emojis;
|
|
4622
|
+
user = null;
|
|
4623
|
+
options;
|
|
4624
|
+
voice;
|
|
4212
4625
|
constructor(options = {}) {
|
|
4213
4626
|
super({ captureRejections: true });
|
|
4214
4627
|
this.options = options;
|
|
@@ -4218,25 +4631,43 @@ var Client = class extends EventEmitter2 {
|
|
|
4218
4631
|
this.servers = new ServerManager(this, options.cacheLimits?.servers);
|
|
4219
4632
|
this.users = new UserManager(this, options.cacheLimits?.users);
|
|
4220
4633
|
this.emojis = new EmojiManager(this, void 0, options.cacheLimits?.emojis);
|
|
4634
|
+
this.voice = new VoiceManager(this);
|
|
4221
4635
|
this.sweepers = new SweeperManager(this, options.sweepers ?? {});
|
|
4222
4636
|
}
|
|
4223
|
-
rest;
|
|
4224
|
-
gateway;
|
|
4225
|
-
channels;
|
|
4226
|
-
servers;
|
|
4227
|
-
users;
|
|
4228
|
-
sweepers;
|
|
4229
|
-
emojis;
|
|
4230
|
-
user = null;
|
|
4231
4637
|
/**
|
|
4232
4638
|
* Connects the bot to the Stoat Gateway
|
|
4233
4639
|
*/
|
|
4234
4640
|
async login(token) {
|
|
4235
4641
|
if (!token) throw new Error("A valid token must be provided.");
|
|
4236
|
-
this.
|
|
4642
|
+
const rootApiUrl = this.options.apiURL ?? "https://api.stoat.chat";
|
|
4643
|
+
this.rest.setBaseURL(rootApiUrl);
|
|
4237
4644
|
this.rest.setToken(token);
|
|
4645
|
+
const configData = await this.fetchConfig(rootApiUrl);
|
|
4646
|
+
const finalWsUrl = this.options.overrides?.wsURL ?? configData.ws;
|
|
4647
|
+
if (!finalWsUrl) {
|
|
4648
|
+
throw new Error(
|
|
4649
|
+
`[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'.`
|
|
4650
|
+
);
|
|
4651
|
+
}
|
|
4652
|
+
this.gateway.setGatewayUrl(finalWsUrl);
|
|
4653
|
+
const finalCdnUrl = this.options.overrides?.cdnURL ?? configData.features.autumn.url;
|
|
4654
|
+
if (!finalCdnUrl) {
|
|
4655
|
+
throw new Error(
|
|
4656
|
+
`[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'.`
|
|
4657
|
+
);
|
|
4658
|
+
}
|
|
4659
|
+
this.rest.setCDNURL(finalCdnUrl);
|
|
4660
|
+
this.sweepers.start();
|
|
4238
4661
|
return this.gateway.connect(token);
|
|
4239
4662
|
}
|
|
4663
|
+
async fetchConfig(baseURL) {
|
|
4664
|
+
try {
|
|
4665
|
+
const response = await fetch(baseURL);
|
|
4666
|
+
return await response.json();
|
|
4667
|
+
} catch (error) {
|
|
4668
|
+
throw new Error(`Failed to fetch ${baseURL}, make sure the instance is running.`);
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4240
4671
|
[/* @__PURE__ */ Symbol.for("nodejs.rejection")](error) {
|
|
4241
4672
|
this.emit("error", error);
|
|
4242
4673
|
}
|
|
@@ -4295,6 +4726,8 @@ export {
|
|
|
4295
4726
|
API,
|
|
4296
4727
|
Attachment,
|
|
4297
4728
|
AttachmentBuilder,
|
|
4729
|
+
AudioPlayer,
|
|
4730
|
+
AudioResource,
|
|
4298
4731
|
Base,
|
|
4299
4732
|
BaseChannel,
|
|
4300
4733
|
BitField,
|
|
@@ -4329,6 +4762,8 @@ export {
|
|
|
4329
4762
|
TextChannel,
|
|
4330
4763
|
UnknownChannel,
|
|
4331
4764
|
User,
|
|
4332
|
-
UserManager
|
|
4765
|
+
UserManager,
|
|
4766
|
+
VoiceConnection,
|
|
4767
|
+
VoiceManager
|
|
4333
4768
|
};
|
|
4334
4769
|
//# sourceMappingURL=index.js.map
|