ryanlink 1.0.1
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/LICENSE +37 -0
- package/README.md +455 -0
- package/dist/index.d.mts +1335 -0
- package/dist/index.d.ts +1335 -0
- package/dist/index.js +4694 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4604 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
- package/src/audio/AudioFilters.ts +316 -0
- package/src/audio/AudioQueue.ts +782 -0
- package/src/audio/AudioTrack.ts +242 -0
- package/src/audio/QueueController.ts +252 -0
- package/src/audio/TrackCollection.ts +138 -0
- package/src/audio/index.ts +9 -0
- package/src/config/defaults.ts +223 -0
- package/src/config/endpoints.ts +99 -0
- package/src/config/index.ts +9 -0
- package/src/config/patterns.ts +55 -0
- package/src/config/presets.ts +400 -0
- package/src/config/symbols.ts +31 -0
- package/src/core/PluginSystem.ts +50 -0
- package/src/core/RyanlinkPlayer.ts +403 -0
- package/src/core/index.ts +6 -0
- package/src/extensions/AutoplayExtension.ts +283 -0
- package/src/extensions/FairPlayExtension.ts +154 -0
- package/src/extensions/LyricsExtension.ts +187 -0
- package/src/extensions/PersistenceExtension.ts +182 -0
- package/src/extensions/SponsorBlockExtension.ts +81 -0
- package/src/extensions/index.ts +9 -0
- package/src/index.ts +19 -0
- package/src/lavalink/ConnectionPool.ts +326 -0
- package/src/lavalink/HttpClient.ts +316 -0
- package/src/lavalink/LavalinkConnection.ts +409 -0
- package/src/lavalink/index.ts +7 -0
- package/src/metadata.ts +88 -0
- package/src/types/api/Rest.ts +949 -0
- package/src/types/api/Websocket.ts +463 -0
- package/src/types/api/index.ts +6 -0
- package/src/types/audio/FilterManager.ts +29 -0
- package/src/types/audio/Queue.ts +4 -0
- package/src/types/audio/QueueManager.ts +30 -0
- package/src/types/audio/index.ts +7 -0
- package/src/types/common.ts +63 -0
- package/src/types/core/Player.ts +322 -0
- package/src/types/core/index.ts +5 -0
- package/src/types/index.ts +6 -0
- package/src/types/lavalink/Node.ts +173 -0
- package/src/types/lavalink/NodeManager.ts +34 -0
- package/src/types/lavalink/REST.ts +144 -0
- package/src/types/lavalink/index.ts +32 -0
- package/src/types/voice/VoiceManager.ts +176 -0
- package/src/types/voice/index.ts +5 -0
- package/src/utils/helpers.ts +169 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/validators.ts +184 -0
- package/src/voice/RegionSelector.ts +184 -0
- package/src/voice/VoiceConnection.ts +451 -0
- package/src/voice/VoiceSession.ts +297 -0
- package/src/voice/index.ts +7 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { NonNullableProp } from "../common";
|
|
2
|
+
import type { CreateQueueOptions } from "../audio";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Common info in Discord's 'dispatch' payload type
|
|
6
|
+
*/
|
|
7
|
+
export interface CommonDispatchPayloadInfo {
|
|
8
|
+
op: 0;
|
|
9
|
+
s: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Discord client ready payload (partial, essential only)
|
|
14
|
+
*/
|
|
15
|
+
export interface BotReadyPayload extends CommonDispatchPayloadInfo {
|
|
16
|
+
t: "READY";
|
|
17
|
+
d: {
|
|
18
|
+
user: {
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Discord voice state update payload
|
|
26
|
+
*/
|
|
27
|
+
export interface VoiceStateUpdatePayload extends CommonDispatchPayloadInfo {
|
|
28
|
+
t: "VOICE_STATE_UPDATE";
|
|
29
|
+
d: {
|
|
30
|
+
guild_id?: string;
|
|
31
|
+
channel_id: string | null;
|
|
32
|
+
user_id: string;
|
|
33
|
+
session_id: string;
|
|
34
|
+
deaf: boolean;
|
|
35
|
+
mute: boolean;
|
|
36
|
+
self_deaf: boolean;
|
|
37
|
+
self_mute: boolean;
|
|
38
|
+
suppress: boolean;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Discord voice server update payload
|
|
44
|
+
*/
|
|
45
|
+
export interface VoiceServerUpdatePayload extends CommonDispatchPayloadInfo {
|
|
46
|
+
t: "VOICE_SERVER_UPDATE";
|
|
47
|
+
d: {
|
|
48
|
+
token: string;
|
|
49
|
+
guild_id: string;
|
|
50
|
+
endpoint: string | null;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Discord dispatch payload
|
|
56
|
+
*/
|
|
57
|
+
export type DiscordDispatchPayload = BotReadyPayload | VoiceStateUpdatePayload | VoiceServerUpdatePayload;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Internal ref representing client-side voice state
|
|
61
|
+
*/
|
|
62
|
+
export interface BotVoiceState
|
|
63
|
+
extends
|
|
64
|
+
Required<NonNullableProp<Omit<VoiceStateUpdatePayload["d"], "guild_id" | "user_id">, "channel_id">>,
|
|
65
|
+
NonNullableProp<Omit<VoiceServerUpdatePayload["d"], "guild_id">, "endpoint"> {
|
|
66
|
+
/**
|
|
67
|
+
* Whether the voice connection is established
|
|
68
|
+
*/
|
|
69
|
+
connected: boolean;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The node session ID this voice state is connected to
|
|
73
|
+
*/
|
|
74
|
+
node_session_id: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the voice connection is reconnecting
|
|
78
|
+
*/
|
|
79
|
+
reconnecting: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The voice region ID (extracted from endpoint)
|
|
83
|
+
*/
|
|
84
|
+
region_id: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Options for customizing the player while connecting
|
|
89
|
+
*/
|
|
90
|
+
export interface ConnectOptions extends Pick<CreateQueueOptions, "context" | "filters" | "node" | "volume"> {}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Discord voice close event codes
|
|
94
|
+
* https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes
|
|
95
|
+
*/
|
|
96
|
+
export const enum VoiceCloseCodes {
|
|
97
|
+
/**
|
|
98
|
+
* You sent an invalid opcode.
|
|
99
|
+
*/
|
|
100
|
+
UnknownOpcode = 4001,
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* You sent an invalid payload in your identifying to the Gateway.
|
|
104
|
+
*/
|
|
105
|
+
FailedToDecodePayload,
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* You sent a payload before identifying with the Gateway.
|
|
109
|
+
*/
|
|
110
|
+
NotAuthenticated,
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The token you sent in your identify payload is incorrect.
|
|
114
|
+
*/
|
|
115
|
+
AuthenticationFailed,
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* You sent more than one identify payload.
|
|
119
|
+
*/
|
|
120
|
+
AlreadyAuthenticated,
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Your session is no longer valid.
|
|
124
|
+
*/
|
|
125
|
+
SessionNoLongerValid,
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Your session has timed out.
|
|
129
|
+
*/
|
|
130
|
+
SessionTimeout = 4009,
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* We can't find the server you're trying to connect to.
|
|
134
|
+
*/
|
|
135
|
+
ServerNotFound = 4011,
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* We didn't recognize the protocol you sent.
|
|
139
|
+
*/
|
|
140
|
+
UnknownProtocol,
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Disconnect individual client (you were kicked, the main gateway session was dropped, etc.). Should not reconnect.
|
|
144
|
+
*/
|
|
145
|
+
Disconnected = 4014,
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The server crashed. Our bad! Try resuming.
|
|
149
|
+
*/
|
|
150
|
+
VoiceServerCrashed,
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* We didn't recognize your encryption.
|
|
154
|
+
*/
|
|
155
|
+
UnknownEncryptionMode,
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* This channel requires a client supporting E2EE via the DAVE Protocol
|
|
159
|
+
*/
|
|
160
|
+
DAVEProtocolRequired,
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* You sent a malformed request.
|
|
164
|
+
*/
|
|
165
|
+
BadRequest = 4020,
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Disconnect due to rate limit exceeded. Should not reconnect.
|
|
169
|
+
*/
|
|
170
|
+
DisconnectedRateLimited,
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Disconnect all clients due to call terminated (channel deleted, voice server changed, etc.). Should not reconnect.
|
|
174
|
+
*/
|
|
175
|
+
DisconnectedCallTerminated,
|
|
176
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for Ryanlink
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Do nothing, return nothing
|
|
7
|
+
* Useful for error handling and default callbacks
|
|
8
|
+
*/
|
|
9
|
+
export const noop = (): void => {};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format duration as a string
|
|
13
|
+
* @param ms Duration in milliseconds
|
|
14
|
+
* @returns Formatted string: `hh:mm:ss`, `mm:ss`, or `00:00` (default)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* formatDuration(0) // "00:00"
|
|
18
|
+
* formatDuration(90000) // "01:30"
|
|
19
|
+
* formatDuration(3661000) // "01:01:01"
|
|
20
|
+
*/
|
|
21
|
+
export const formatDuration = (ms: number): string => {
|
|
22
|
+
if (!Number.isSafeInteger(ms) || ms <= 0) {
|
|
23
|
+
return "00:00";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const s = Math.floor(ms / 1000);
|
|
27
|
+
const ss = `${s % 60}`.padStart(2, "0");
|
|
28
|
+
const mm = `${Math.floor(s / 60) % 60}`.padStart(2, "0");
|
|
29
|
+
|
|
30
|
+
if (s < 3600) {
|
|
31
|
+
return `${(s === 3600 ? "01:" : "") + mm}:${ss}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return `${`${Math.floor(s / 3600)}`.padStart(2, "0")}:${mm}:${ss}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse duration string to milliseconds
|
|
39
|
+
* @param duration Duration string (hh:mm:ss, mm:ss, or ss)
|
|
40
|
+
* @returns Duration in milliseconds
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* parseDuration("01:30") // 90000
|
|
44
|
+
* parseDuration("1:01:01") // 3661000
|
|
45
|
+
*/
|
|
46
|
+
export const parseDuration = (duration: string): number => {
|
|
47
|
+
const parts = duration.split(":").map(Number);
|
|
48
|
+
|
|
49
|
+
if (parts.length === 1) {
|
|
50
|
+
return (parts[0] ?? 0) * 1000;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (parts.length === 2) {
|
|
54
|
+
return ((parts[0] ?? 0) * 60 + (parts[1] ?? 0)) * 1000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (parts.length === 3) {
|
|
58
|
+
return ((parts[0] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[2] ?? 0)) * 1000;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return 0;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clamp a number between min and max
|
|
66
|
+
* @param value Value to clamp
|
|
67
|
+
* @param min Minimum value
|
|
68
|
+
* @param max Maximum value
|
|
69
|
+
* @returns Clamped value
|
|
70
|
+
*/
|
|
71
|
+
export const clamp = (value: number, min: number, max: number): number => {
|
|
72
|
+
return Math.min(Math.max(value, min), max);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sleep for a specified duration
|
|
77
|
+
* @param ms Duration in milliseconds
|
|
78
|
+
* @returns Promise that resolves after the duration
|
|
79
|
+
*/
|
|
80
|
+
export const sleep = (ms: number): Promise<void> => {
|
|
81
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retry a function with exponential backoff
|
|
86
|
+
* @param fn Function to retry
|
|
87
|
+
* @param maxAttempts Maximum number of attempts
|
|
88
|
+
* @param baseDelay Base delay in milliseconds
|
|
89
|
+
* @returns Result of the function
|
|
90
|
+
*/
|
|
91
|
+
export const retry = async <T>(fn: () => Promise<T>, maxAttempts = 3, baseDelay = 1000): Promise<T> => {
|
|
92
|
+
let lastError: Error | undefined;
|
|
93
|
+
|
|
94
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
95
|
+
try {
|
|
96
|
+
return await fn();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
99
|
+
|
|
100
|
+
if (attempt < maxAttempts - 1) {
|
|
101
|
+
const delay = baseDelay * Math.pow(2, attempt);
|
|
102
|
+
await sleep(delay);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw lastError ?? new Error("Retry failed");
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Shuffle an array in place using Fisher-Yates algorithm
|
|
112
|
+
* @param array Array to shuffle
|
|
113
|
+
* @returns The shuffled array
|
|
114
|
+
*/
|
|
115
|
+
export const shuffle = <T>(array: T[]): T[] => {
|
|
116
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
117
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
118
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
119
|
+
}
|
|
120
|
+
return array;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get a random element from an array
|
|
125
|
+
* @param array Array to pick from
|
|
126
|
+
* @returns Random element or undefined if array is empty
|
|
127
|
+
*/
|
|
128
|
+
export const randomElement = <T>(array: T[]): T | undefined => {
|
|
129
|
+
if (array.length === 0) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Chunk an array into smaller arrays
|
|
137
|
+
* @param array Array to chunk
|
|
138
|
+
* @param size Size of each chunk
|
|
139
|
+
* @returns Array of chunks
|
|
140
|
+
*/
|
|
141
|
+
export const chunk = <T>(array: T[], size: number): T[][] => {
|
|
142
|
+
const chunks: T[][] = [];
|
|
143
|
+
for (let i = 0; i < array.length; i += size) {
|
|
144
|
+
chunks.push(array.slice(i, i + size));
|
|
145
|
+
}
|
|
146
|
+
return chunks;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Remove duplicates from an array
|
|
151
|
+
* @param array Array to deduplicate
|
|
152
|
+
* @param key Optional key function for complex objects
|
|
153
|
+
* @returns Array without duplicates
|
|
154
|
+
*/
|
|
155
|
+
export const unique = <T>(array: T[], key?: (item: T) => unknown): T[] => {
|
|
156
|
+
if (!key) {
|
|
157
|
+
return [...new Set(array)];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
return array.filter((item) => {
|
|
162
|
+
const k = key(item);
|
|
163
|
+
if (seen.has(k)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
seen.add(k);
|
|
167
|
+
return true;
|
|
168
|
+
});
|
|
169
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { URL } from "node:url";
|
|
2
|
+
import type { CreateNodeOptions, PlayerOptions } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validation functions for Ryanlink
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if input is a finite number
|
|
10
|
+
* @param input Value to check
|
|
11
|
+
* @param check Optional check type: `integer`, `natural` (> 0), `whole` (>= 0)
|
|
12
|
+
* @returns `true` if the input passed, `false` otherwise
|
|
13
|
+
*/
|
|
14
|
+
export const isNumber = <T extends number>(
|
|
15
|
+
input: unknown,
|
|
16
|
+
check?: "integer" | "natural" | "whole" | "safe-int" | "positive",
|
|
17
|
+
): input is T => {
|
|
18
|
+
if (check === undefined) {
|
|
19
|
+
return Number.isFinite(input);
|
|
20
|
+
}
|
|
21
|
+
if (check === "integer" || check === "safe-int") {
|
|
22
|
+
return Number.isSafeInteger(input);
|
|
23
|
+
}
|
|
24
|
+
if (check === "natural" || check === "positive") {
|
|
25
|
+
return Number.isSafeInteger(input) && (input as number) > 0;
|
|
26
|
+
}
|
|
27
|
+
if (check === "whole") {
|
|
28
|
+
return Number.isSafeInteger(input) && (input as number) >= 0;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if input is a string
|
|
35
|
+
* @param input Value to check
|
|
36
|
+
* @param check Optional check: {@linkcode RegExp}, `url` ({@linkcode URL.canParse}), `non-empty` (at least one non-whitespace character)
|
|
37
|
+
* @returns `true` if the input passed, `false` otherwise
|
|
38
|
+
*/
|
|
39
|
+
export const isString = <T extends string>(input: unknown, check?: "url" | "non-empty" | RegExp): input is T => {
|
|
40
|
+
if (typeof input !== "string") {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (check === undefined) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (check === "url") {
|
|
47
|
+
return URL.canParse(input);
|
|
48
|
+
}
|
|
49
|
+
if (check === "non-empty") {
|
|
50
|
+
return input.trim().length > 0;
|
|
51
|
+
}
|
|
52
|
+
if (check instanceof RegExp) {
|
|
53
|
+
return check.test(input);
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if input is a plain object
|
|
60
|
+
* @param input Value to check
|
|
61
|
+
* @param check Optional check: `non-empty` (at least one key)
|
|
62
|
+
* @returns `true` if the input passed, `false` otherwise
|
|
63
|
+
*/
|
|
64
|
+
export const isRecord = <T extends Record<string, unknown>>(input: unknown, check?: "non-empty"): input is T => {
|
|
65
|
+
if (!input || input.constructor !== Object) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (check === undefined) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (check === "non-empty") {
|
|
72
|
+
return Object.keys(input).length > 0;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if input is an array
|
|
79
|
+
* @param input Value to check
|
|
80
|
+
* @param check Optional check: `non-empty`, or a function (same as {@linkcode Array.prototype.every})
|
|
81
|
+
* @returns `true` if the input passed, `false` otherwise
|
|
82
|
+
*/
|
|
83
|
+
export const isArray = <T extends unknown[]>(
|
|
84
|
+
input: unknown,
|
|
85
|
+
check?: "non-empty" | Parameters<T["every"]>[0],
|
|
86
|
+
): input is T => {
|
|
87
|
+
if (!Array.isArray(input)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (check === undefined) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (check === "non-empty") {
|
|
94
|
+
return input.length !== 0;
|
|
95
|
+
}
|
|
96
|
+
if (typeof check === "function") {
|
|
97
|
+
return input.every(check);
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if input is a boolean
|
|
104
|
+
* @param input Value to check
|
|
105
|
+
* @returns `true` if the input is a boolean, `false` otherwise
|
|
106
|
+
*/
|
|
107
|
+
export const isBoolean = (input: unknown): input is boolean => {
|
|
108
|
+
return typeof input === "boolean";
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if input is a function
|
|
113
|
+
* @param input Value to check
|
|
114
|
+
* @returns `true` if the input is a function, `false` otherwise
|
|
115
|
+
*/
|
|
116
|
+
export const isFunction = (input: unknown): input is (...args: unknown[]) => unknown => {
|
|
117
|
+
return typeof input === "function";
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if input is null or undefined
|
|
122
|
+
* @param input Value to check
|
|
123
|
+
* @returns `true` if the input is null or undefined, `false` otherwise
|
|
124
|
+
*/
|
|
125
|
+
export const isNullish = (input: unknown): input is null | undefined => {
|
|
126
|
+
return input === null || input === undefined;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if input is a valid snowflake (Discord ID)
|
|
131
|
+
* @param input Value to check
|
|
132
|
+
* @returns `true` if the input is a valid snowflake, `false` otherwise
|
|
133
|
+
*/
|
|
134
|
+
export const isSnowflake = (input: unknown): input is string => {
|
|
135
|
+
return isString(input) && /^\d{17,20}$/.test(input);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if input is a valid URL
|
|
140
|
+
* @param input Value to check
|
|
141
|
+
* @returns `true` if the input is a valid URL, `false` otherwise
|
|
142
|
+
*/
|
|
143
|
+
export const isUrl = (input: unknown): input is string => {
|
|
144
|
+
return isString(input, "url");
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Type guard to check if error is an Error instance
|
|
149
|
+
* @param error Value to check
|
|
150
|
+
* @returns `true` if the value is an Error, `false` otherwise
|
|
151
|
+
*/
|
|
152
|
+
export const isError = (error: unknown): error is Error => {
|
|
153
|
+
return error instanceof Error;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Assert that a condition is true, throw error otherwise
|
|
158
|
+
*/
|
|
159
|
+
export function assert(condition: unknown, message: string): asserts condition {
|
|
160
|
+
if (!condition) {
|
|
161
|
+
throw new Error(message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validates node options
|
|
167
|
+
*/
|
|
168
|
+
export function validateNodeOptions(options: CreateNodeOptions): void {
|
|
169
|
+
assert(isString(options.name, "non-empty"), "Node name must be a non-empty string");
|
|
170
|
+
assert(isString(options.host, "non-empty"), "Node host must be a non-empty string");
|
|
171
|
+
assert(isNumber(options.port, "natural"), "Node port must be a positive number");
|
|
172
|
+
assert(isString(options.password, "non-empty"), "Node password must be a non-empty string");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validates player options
|
|
177
|
+
*/
|
|
178
|
+
export function validatePlayerOptions(options: PlayerOptions): void {
|
|
179
|
+
assert(isArray(options.nodes, "non-empty"), "At least one node is required");
|
|
180
|
+
assert(isFunction(options.forwardVoiceUpdate), "forwardVoiceUpdate must be a function");
|
|
181
|
+
if (options.queryPrefix !== undefined) {
|
|
182
|
+
assert(isString(options.queryPrefix, "non-empty"), "queryPrefix must be a non-empty string");
|
|
183
|
+
}
|
|
184
|
+
}
|