mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-12 09:33:32 +01:00
chore: monorepo setup (#7175)
This commit is contained in:
56
packages/voice/src/util/Secretbox.ts
Normal file
56
packages/voice/src/util/Secretbox.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
interface Methods {
|
||||
open(buffer: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer | null;
|
||||
close(opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer;
|
||||
random(bytes: number, nonce: Buffer): Buffer;
|
||||
}
|
||||
|
||||
const libs = {
|
||||
sodium: (sodium: any): Methods => ({
|
||||
open: sodium.api.crypto_secretbox_open_easy,
|
||||
close: sodium.api.crypto_secretbox_easy,
|
||||
random: (n: any, buffer?: Buffer) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
if (!buffer) buffer = Buffer.allocUnsafe(n);
|
||||
sodium.api.randombytes_buf(buffer);
|
||||
return buffer;
|
||||
},
|
||||
}),
|
||||
'libsodium-wrappers': (sodium: any): Methods => ({
|
||||
open: sodium.crypto_secretbox_open_easy,
|
||||
close: sodium.crypto_secretbox_easy,
|
||||
random: (n: any) => sodium.randombytes_buf(n),
|
||||
}),
|
||||
tweetnacl: (tweetnacl: any): Methods => ({
|
||||
open: tweetnacl.secretbox.open,
|
||||
close: tweetnacl.secretbox,
|
||||
random: (n: any) => tweetnacl.randomBytes(n),
|
||||
}),
|
||||
} as const;
|
||||
|
||||
const fallbackError = () => {
|
||||
throw new Error(
|
||||
`Cannot play audio as no valid encryption package is installed.
|
||||
- Install sodium, libsodium-wrappers, or tweetnacl.
|
||||
- Use the generateDependencyReport() function for more information.\n`,
|
||||
);
|
||||
};
|
||||
|
||||
const methods: Methods = {
|
||||
open: fallbackError,
|
||||
close: fallbackError,
|
||||
random: fallbackError,
|
||||
};
|
||||
|
||||
void (async () => {
|
||||
for (const libName of Object.keys(libs) as (keyof typeof libs)[]) {
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
const lib = require(libName);
|
||||
if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready;
|
||||
Object.assign(methods, libs[libName](lib));
|
||||
break;
|
||||
} catch {}
|
||||
}
|
||||
})();
|
||||
|
||||
export { methods };
|
||||
16
packages/voice/src/util/__tests__/Secretbox.test.ts
Normal file
16
packages/voice/src/util/__tests__/Secretbox.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { methods } from '../Secretbox';
|
||||
jest.mock(
|
||||
'tweetnacl',
|
||||
() => ({
|
||||
secretbox: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
open() {},
|
||||
},
|
||||
}),
|
||||
{ virtual: true },
|
||||
);
|
||||
|
||||
test('Does not throw error with a package installed', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => methods.open()).not.toThrowError();
|
||||
});
|
||||
24
packages/voice/src/util/__tests__/abortAfter.test.ts
Normal file
24
packages/voice/src/util/__tests__/abortAfter.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { abortAfter } from '../abortAfter';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
|
||||
|
||||
describe('abortAfter', () => {
|
||||
test('Aborts after the given delay', () => {
|
||||
const [ac, signal] = abortAfter(100);
|
||||
expect(ac.signal).toBe(signal);
|
||||
expect(signal.aborted).toBe(false);
|
||||
jest.runAllTimers();
|
||||
expect(signal.aborted).toBe(true);
|
||||
});
|
||||
|
||||
test('Cleans up when manually aborted', () => {
|
||||
const [ac, signal] = abortAfter(100);
|
||||
expect(ac.signal).toBe(signal);
|
||||
expect(signal.aborted).toBe(false);
|
||||
clearTimeoutSpy.mockClear();
|
||||
ac.abort();
|
||||
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
122
packages/voice/src/util/__tests__/demuxProbe.test.ts
Normal file
122
packages/voice/src/util/__tests__/demuxProbe.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { demuxProbe } from '../demuxProbe';
|
||||
import { opus as _opus } from 'prism-media';
|
||||
import { Readable } from 'node:stream';
|
||||
import { StreamType } from '../../audio';
|
||||
import EventEmitter, { once } from 'node:events';
|
||||
|
||||
jest.mock('prism-media');
|
||||
|
||||
const WebmDemuxer = _opus.WebmDemuxer as unknown as jest.Mock<_opus.WebmDemuxer>;
|
||||
const OggDemuxer = _opus.OggDemuxer as unknown as jest.Mock<_opus.OggDemuxer>;
|
||||
|
||||
async function* gen(n: number) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
yield Buffer.from([i]);
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
function range(n: number) {
|
||||
return Buffer.from(Array.from(Array(n).keys()));
|
||||
}
|
||||
|
||||
const validHead = Buffer.from([
|
||||
0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x02, 0x38, 0x01, 0x80, 0xbb, 0, 0, 0, 0, 0,
|
||||
]);
|
||||
|
||||
const invalidHead = Buffer.from([
|
||||
0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x01, 0x38, 0x01, 0x80, 0xbb, 0, 0, 0, 0, 0,
|
||||
]);
|
||||
|
||||
async function collectStream(stream: Readable): Promise<Buffer> {
|
||||
let output = Buffer.alloc(0);
|
||||
await once(stream, 'readable');
|
||||
for await (const data of stream) {
|
||||
output = Buffer.concat([output, data]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function nextTick() {
|
||||
return new Promise((resolve) => process.nextTick(resolve));
|
||||
}
|
||||
|
||||
describe('demuxProbe', () => {
|
||||
const webmWrite: jest.Mock<(buffer: Buffer) => void> = jest.fn();
|
||||
const oggWrite: jest.Mock<(buffer: Buffer) => void> = jest.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
WebmDemuxer.prototype = {
|
||||
...WebmDemuxer,
|
||||
...EventEmitter.prototype,
|
||||
write: webmWrite,
|
||||
};
|
||||
OggDemuxer.prototype = {
|
||||
...OggDemuxer,
|
||||
...EventEmitter.prototype,
|
||||
write: oggWrite,
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
webmWrite.mockReset();
|
||||
oggWrite.mockReset();
|
||||
});
|
||||
|
||||
test('Defaults to arbitrary', async () => {
|
||||
const stream = Readable.from(gen(10), { objectMode: false });
|
||||
const probe = await demuxProbe(stream);
|
||||
expect(probe.type).toBe(StreamType.Arbitrary);
|
||||
await expect(collectStream(probe.stream)).resolves.toEqual(range(10));
|
||||
});
|
||||
|
||||
test('Detects WebM', async () => {
|
||||
const stream = Readable.from(gen(10), { objectMode: false });
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
webmWrite.mockImplementation(function mock(data: Buffer) {
|
||||
if (data[0] === 5) this.emit('head', validHead);
|
||||
} as any);
|
||||
const probe = await demuxProbe(stream);
|
||||
expect(probe.type).toBe(StreamType.WebmOpus);
|
||||
await expect(collectStream(probe.stream)).resolves.toEqual(range(10));
|
||||
});
|
||||
|
||||
test('Detects Ogg', async () => {
|
||||
const stream = Readable.from(gen(10), { objectMode: false });
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
oggWrite.mockImplementation(function mock(data: Buffer) {
|
||||
if (data[0] === 5) this.emit('head', validHead);
|
||||
} as any);
|
||||
const probe = await demuxProbe(stream);
|
||||
expect(probe.type).toBe(StreamType.OggOpus);
|
||||
await expect(collectStream(probe.stream)).resolves.toEqual(range(10));
|
||||
});
|
||||
|
||||
test('Rejects invalid OpusHead', async () => {
|
||||
const stream = Readable.from(gen(10), { objectMode: false });
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
oggWrite.mockImplementation(function mock(data: Buffer) {
|
||||
if (data[0] === 5) this.emit('head', invalidHead);
|
||||
} as any);
|
||||
const probe = await demuxProbe(stream);
|
||||
expect(probe.type).toBe(StreamType.Arbitrary);
|
||||
await expect(collectStream(probe.stream)).resolves.toEqual(range(10));
|
||||
});
|
||||
|
||||
test('Gives up on larger streams', async () => {
|
||||
const stream = Readable.from(gen(8192), { objectMode: false });
|
||||
const probe = await demuxProbe(stream);
|
||||
expect(probe.type).toBe(StreamType.Arbitrary);
|
||||
await expect(collectStream(probe.stream)).resolves.toEqual(range(8192));
|
||||
});
|
||||
|
||||
test('Propagates errors', async () => {
|
||||
const testError = new Error('test error');
|
||||
const stream = new Readable({
|
||||
read() {
|
||||
this.destroy(testError);
|
||||
},
|
||||
});
|
||||
await expect(demuxProbe(stream)).rejects.toBe(testError);
|
||||
});
|
||||
});
|
||||
54
packages/voice/src/util/__tests__/entersState.test.ts
Normal file
54
packages/voice/src/util/__tests__/entersState.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import EventEmitter from 'node:events';
|
||||
import { VoiceConnection, VoiceConnectionStatus } from '../../VoiceConnection';
|
||||
import { entersState } from '../entersState';
|
||||
|
||||
function createFakeVoiceConnection(status = VoiceConnectionStatus.Signalling) {
|
||||
const vc = new EventEmitter() as any;
|
||||
vc.state = { status };
|
||||
return vc as VoiceConnection;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
describe('entersState', () => {
|
||||
test('Returns the target once the state has been entered before timeout', async () => {
|
||||
jest.useRealTimers();
|
||||
const vc = createFakeVoiceConnection();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
process.nextTick(() => vc.emit(VoiceConnectionStatus.Ready, null as any, null as any));
|
||||
const result = await entersState(vc, VoiceConnectionStatus.Ready, 1000);
|
||||
expect(result).toBe(vc);
|
||||
});
|
||||
|
||||
test('Rejects once the timeout is exceeded', async () => {
|
||||
const vc = createFakeVoiceConnection();
|
||||
const promise = entersState(vc, VoiceConnectionStatus.Ready, 1000);
|
||||
jest.runAllTimers();
|
||||
await expect(promise).rejects.toThrowError();
|
||||
});
|
||||
|
||||
test('Returns the target once the state has been entered before signal is aborted', async () => {
|
||||
jest.useRealTimers();
|
||||
const vc = createFakeVoiceConnection();
|
||||
const ac = new AbortController();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
process.nextTick(() => vc.emit(VoiceConnectionStatus.Ready, null as any, null as any));
|
||||
const result = await entersState(vc, VoiceConnectionStatus.Ready, ac.signal);
|
||||
expect(result).toBe(vc);
|
||||
});
|
||||
|
||||
test('Rejects once the signal is aborted', async () => {
|
||||
const vc = createFakeVoiceConnection();
|
||||
const ac = new AbortController();
|
||||
const promise = entersState(vc, VoiceConnectionStatus.Ready, ac.signal);
|
||||
ac.abort();
|
||||
await expect(promise).rejects.toThrowError();
|
||||
});
|
||||
|
||||
test('Resolves immediately when target already in desired state', async () => {
|
||||
const vc = createFakeVoiceConnection();
|
||||
await expect(entersState(vc, VoiceConnectionStatus.Signalling, 1000)).resolves.toBe(vc);
|
||||
});
|
||||
});
|
||||
12
packages/voice/src/util/abortAfter.ts
Normal file
12
packages/voice/src/util/abortAfter.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Creates an abort controller that aborts after the given time.
|
||||
*
|
||||
* @param delay - The time in milliseconds to wait before aborting
|
||||
*/
|
||||
export function abortAfter(delay: number): [AbortController, AbortSignal] {
|
||||
const ac = new AbortController();
|
||||
const timeout = setTimeout(() => ac.abort(), delay);
|
||||
// @ts-ignore
|
||||
ac.signal.addEventListener('abort', () => clearTimeout(timeout));
|
||||
return [ac, ac.signal];
|
||||
}
|
||||
53
packages/voice/src/util/adapter.ts
Normal file
53
packages/voice/src/util/adapter.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v9';
|
||||
|
||||
/**
|
||||
* Methods that are provided by the @discordjs/voice library to implementations of
|
||||
* Discord gateway DiscordGatewayAdapters.
|
||||
*/
|
||||
export interface DiscordGatewayAdapterLibraryMethods {
|
||||
/**
|
||||
* Call this when you receive a VOICE_SERVER_UPDATE payload that is relevant to the adapter.
|
||||
*
|
||||
* @param data - The inner data of the VOICE_SERVER_UPDATE payload
|
||||
*/
|
||||
onVoiceServerUpdate(data: GatewayVoiceServerUpdateDispatchData): void;
|
||||
/**
|
||||
* Call this when you receive a VOICE_STATE_UPDATE payload that is relevant to the adapter.
|
||||
*
|
||||
* @param data - The inner data of the VOICE_STATE_UPDATE payload
|
||||
*/
|
||||
onVoiceStateUpdate(data: GatewayVoiceStateUpdateDispatchData): void;
|
||||
/**
|
||||
* Call this when the adapter can no longer be used (e.g. due to a disconnect from the main gateway)
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods that are provided by the implementer of a Discord gateway DiscordGatewayAdapter.
|
||||
*/
|
||||
export interface DiscordGatewayAdapterImplementerMethods {
|
||||
/**
|
||||
* Implement this method such that the given payload is sent to the main Discord gateway connection.
|
||||
*
|
||||
* @param payload - The payload to send to the main Discord gateway connection
|
||||
*
|
||||
* @returns `false` if the payload definitely failed to send - in this case, the voice connection disconnects
|
||||
*/
|
||||
sendPayload(payload: any): boolean;
|
||||
/**
|
||||
* This will be called by @discordjs/voice when the adapter can safely be destroyed as it will no
|
||||
* longer be used.
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function used to build adapters. It accepts a methods parameter that contains functions that
|
||||
* can be called by the implementer when new data is received on its gateway connection. In return,
|
||||
* the implementer will return some methods that the library can call - e.g. to send messages on
|
||||
* the gateway, or to signal that the adapter can be removed.
|
||||
*/
|
||||
export type DiscordGatewayAdapterCreator = (
|
||||
methods: DiscordGatewayAdapterLibraryMethods,
|
||||
) => DiscordGatewayAdapterImplementerMethods;
|
||||
118
packages/voice/src/util/demuxProbe.ts
Normal file
118
packages/voice/src/util/demuxProbe.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Readable } from 'node:stream';
|
||||
import prism from 'prism-media';
|
||||
import { noop } from './util';
|
||||
import { StreamType } from '..';
|
||||
|
||||
/**
|
||||
* Takes an Opus Head, and verifies whether the associated Opus audio is suitable to play in a Discord voice channel.
|
||||
*
|
||||
* @param opusHead The Opus Head to validate
|
||||
*
|
||||
* @returns `true` if suitable to play in a Discord voice channel, otherwise `false`
|
||||
*/
|
||||
export function validateDiscordOpusHead(opusHead: Buffer): boolean {
|
||||
const channels = opusHead.readUInt8(9);
|
||||
const sampleRate = opusHead.readUInt32LE(12);
|
||||
return channels === 2 && sampleRate === 48000;
|
||||
}
|
||||
|
||||
/**
|
||||
* The resulting information after probing an audio stream
|
||||
*/
|
||||
export interface ProbeInfo {
|
||||
/**
|
||||
* The readable audio stream to use. You should use this rather than the input stream, as the probing
|
||||
* function can sometimes read the input stream to its end and cause the stream to close.
|
||||
*/
|
||||
stream: Readable;
|
||||
|
||||
/**
|
||||
* The recommended stream type for this audio stream.
|
||||
*/
|
||||
type: StreamType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to probe a readable stream to figure out whether it can be demuxed using an Ogg or WebM Opus demuxer.
|
||||
*
|
||||
* @param stream The readable stream to probe
|
||||
* @param probeSize The number of bytes to attempt to read before giving up on the probe
|
||||
* @param validator The Opus Head validator function
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export function demuxProbe(
|
||||
stream: Readable,
|
||||
probeSize = 1024,
|
||||
validator = validateDiscordOpusHead,
|
||||
): Promise<ProbeInfo> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Preconditions
|
||||
if (stream.readableObjectMode) reject(new Error('Cannot probe a readable stream in object mode'));
|
||||
if (stream.readableEnded) reject(new Error('Cannot probe a stream that has ended'));
|
||||
|
||||
let readBuffer = Buffer.alloc(0);
|
||||
|
||||
let resolved: StreamType | undefined = undefined;
|
||||
|
||||
const finish = (type: StreamType) => {
|
||||
stream.off('data', onData);
|
||||
stream.off('close', onClose);
|
||||
stream.off('end', onClose);
|
||||
stream.pause();
|
||||
resolved = type;
|
||||
if (stream.readableEnded) {
|
||||
resolve({
|
||||
stream: Readable.from(readBuffer),
|
||||
type,
|
||||
});
|
||||
} else {
|
||||
if (readBuffer.length > 0) {
|
||||
stream.push(readBuffer);
|
||||
}
|
||||
resolve({
|
||||
stream,
|
||||
type,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const foundHead = (type: StreamType) => (head: Buffer) => {
|
||||
if (validator(head)) {
|
||||
finish(type);
|
||||
}
|
||||
};
|
||||
|
||||
const webm = new prism.opus.WebmDemuxer();
|
||||
webm.once('error', noop);
|
||||
webm.on('head', foundHead(StreamType.WebmOpus));
|
||||
|
||||
const ogg = new prism.opus.OggDemuxer();
|
||||
ogg.once('error', noop);
|
||||
ogg.on('head', foundHead(StreamType.OggOpus));
|
||||
|
||||
const onClose = () => {
|
||||
if (!resolved) {
|
||||
finish(StreamType.Arbitrary);
|
||||
}
|
||||
};
|
||||
|
||||
const onData = (buffer: Buffer) => {
|
||||
readBuffer = Buffer.concat([readBuffer, buffer]);
|
||||
|
||||
webm.write(buffer);
|
||||
ogg.write(buffer);
|
||||
|
||||
if (readBuffer.length >= probeSize) {
|
||||
stream.off('data', onData);
|
||||
stream.pause();
|
||||
process.nextTick(onClose);
|
||||
}
|
||||
};
|
||||
|
||||
stream.once('error', reject);
|
||||
stream.on('data', onData);
|
||||
stream.once('close', onClose);
|
||||
stream.once('end', onClose);
|
||||
});
|
||||
}
|
||||
54
packages/voice/src/util/entersState.ts
Normal file
54
packages/voice/src/util/entersState.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { VoiceConnection, VoiceConnectionStatus } from '../VoiceConnection';
|
||||
import type { AudioPlayer, AudioPlayerStatus } from '../audio/AudioPlayer';
|
||||
import { abortAfter } from './abortAfter';
|
||||
import EventEmitter, { once } from 'node:events';
|
||||
|
||||
/**
|
||||
* Allows a voice connection a specified amount of time to enter a given state, otherwise rejects with an error.
|
||||
*
|
||||
* @param target - The voice connection that we want to observe the state change for
|
||||
* @param status - The status that the voice connection should be in
|
||||
* @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation
|
||||
*/
|
||||
export function entersState(
|
||||
target: VoiceConnection,
|
||||
status: VoiceConnectionStatus,
|
||||
timeoutOrSignal: number | AbortSignal,
|
||||
): Promise<VoiceConnection>;
|
||||
|
||||
/**
|
||||
* Allows an audio player a specified amount of time to enter a given state, otherwise rejects with an error.
|
||||
*
|
||||
* @param target - The audio player that we want to observe the state change for
|
||||
* @param status - The status that the audio player should be in
|
||||
* @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation
|
||||
*/
|
||||
export function entersState(
|
||||
target: AudioPlayer,
|
||||
status: AudioPlayerStatus,
|
||||
timeoutOrSignal: number | AbortSignal,
|
||||
): Promise<AudioPlayer>;
|
||||
|
||||
/**
|
||||
* Allows a target a specified amount of time to enter a given state, otherwise rejects with an error.
|
||||
*
|
||||
* @param target - The object that we want to observe the state change for
|
||||
* @param status - The status that the target should be in
|
||||
* @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation
|
||||
*/
|
||||
export async function entersState<T extends VoiceConnection | AudioPlayer>(
|
||||
target: T,
|
||||
status: VoiceConnectionStatus | AudioPlayerStatus,
|
||||
timeoutOrSignal: number | AbortSignal,
|
||||
) {
|
||||
if (target.state.status !== status) {
|
||||
const [ac, signal] =
|
||||
typeof timeoutOrSignal === 'number' ? abortAfter(timeoutOrSignal) : [undefined, timeoutOrSignal];
|
||||
try {
|
||||
await once(target as EventEmitter, status, { signal });
|
||||
} finally {
|
||||
ac?.abort();
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
83
packages/voice/src/util/generateDependencyReport.ts
Normal file
83
packages/voice/src/util/generateDependencyReport.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import prism from 'prism-media';
|
||||
|
||||
/**
|
||||
* Generates a report of the dependencies used by the \@discordjs/voice module.
|
||||
* Useful for debugging.
|
||||
*/
|
||||
export function generateDependencyReport() {
|
||||
const report = [];
|
||||
const addVersion = (name: string) => report.push(`- ${name}: ${version(name)}`);
|
||||
// general
|
||||
report.push('Core Dependencies');
|
||||
addVersion('@discordjs/voice');
|
||||
addVersion('prism-media');
|
||||
report.push('');
|
||||
|
||||
// opus
|
||||
report.push('Opus Libraries');
|
||||
addVersion('@discordjs/opus');
|
||||
addVersion('opusscript');
|
||||
report.push('');
|
||||
|
||||
// encryption
|
||||
report.push('Encryption Libraries');
|
||||
addVersion('sodium');
|
||||
addVersion('libsodium-wrappers');
|
||||
addVersion('tweetnacl');
|
||||
report.push('');
|
||||
|
||||
// ffmpeg
|
||||
report.push('FFmpeg');
|
||||
try {
|
||||
const info = prism.FFmpeg.getInfo();
|
||||
report.push(`- version: ${info.version}`);
|
||||
report.push(`- libopus: ${info.output.includes('--enable-libopus') ? 'yes' : 'no'}`);
|
||||
} catch (err) {
|
||||
report.push('- not found');
|
||||
}
|
||||
|
||||
return ['-'.repeat(50), ...report, '-'.repeat(50)].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the package.json file for a given module.
|
||||
*
|
||||
* @param dir - The directory to look in
|
||||
* @param packageName - The name of the package to look for
|
||||
* @param depth - The maximum recursion depth
|
||||
*/
|
||||
function findPackageJSON(
|
||||
dir: string,
|
||||
packageName: string,
|
||||
depth: number,
|
||||
): { name: string; version: string } | undefined {
|
||||
if (depth === 0) return undefined;
|
||||
const attemptedPath = resolve(dir, './package.json');
|
||||
try {
|
||||
const pkg = require(attemptedPath);
|
||||
if (pkg.name !== packageName) throw new Error('package.json does not match');
|
||||
return pkg;
|
||||
} catch (err) {
|
||||
return findPackageJSON(resolve(dir, '..'), packageName, depth - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the version of a dependency.
|
||||
*
|
||||
* @param name - The package to find the version of
|
||||
*/
|
||||
function version(name: string): string {
|
||||
try {
|
||||
const pkg =
|
||||
name === '@discordjs/voice'
|
||||
? require('../../package.json')
|
||||
: findPackageJSON(dirname(require.resolve(name)), name, 3);
|
||||
return pkg?.version ?? 'not found';
|
||||
} catch (err) {
|
||||
return 'not found';
|
||||
}
|
||||
}
|
||||
4
packages/voice/src/util/index.ts
Normal file
4
packages/voice/src/util/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './generateDependencyReport';
|
||||
export * from './entersState';
|
||||
export * from './adapter';
|
||||
export * from './demuxProbe';
|
||||
4
packages/voice/src/util/util.ts
Normal file
4
packages/voice/src/util/util.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
export const noop = () => {};
|
||||
|
||||
export type Awaited<T> = T | Promise<T>;
|
||||
Reference in New Issue
Block a user