Queue & Search commands
General improvements to everything else
This commit is contained in:
parent
699ba21b88
commit
2e291c514b
1
@types/DiscordClient.d.ts
vendored
1
@types/DiscordClient.d.ts
vendored
@ -35,6 +35,7 @@ export type CommandDefinition = {
|
|||||||
guildOnly?: boolean,
|
guildOnly?: boolean,
|
||||||
help?: string,
|
help?: string,
|
||||||
limited?: Snowflake[],
|
limited?: Snowflake[],
|
||||||
|
showUsage?: boolean
|
||||||
} & Omit<ComponentOptions, 'type'>
|
} & Omit<ComponentOptions, 'type'>
|
||||||
|
|
||||||
export type ObserverOptions = {
|
export type ObserverOptions = {
|
||||||
|
15
@types/MusicPlayer.d.ts
vendored
15
@types/MusicPlayer.d.ts
vendored
@ -15,4 +15,19 @@ export type MusicIndexEntry = {
|
|||||||
album?: string,
|
album?: string,
|
||||||
year?: number,
|
year?: number,
|
||||||
file: string,
|
file: string,
|
||||||
|
stats: {
|
||||||
|
plays: number,
|
||||||
|
skips: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MusicQuery = {
|
||||||
|
title?: string
|
||||||
|
artist?: string,
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueueOrder = {
|
||||||
|
artist?: string,
|
||||||
|
title: string
|
||||||
}
|
}
|
@ -22,11 +22,13 @@
|
|||||||
"@navy.gif/logger": "^2.5.4",
|
"@navy.gif/logger": "^2.5.4",
|
||||||
"@navy.gif/timestring": "^6.0.6",
|
"@navy.gif/timestring": "^6.0.6",
|
||||||
"bufferutil": "^4.0.8",
|
"bufferutil": "^4.0.8",
|
||||||
|
"common-tags": "^1.8.2",
|
||||||
"discord.js": "^14.14.1",
|
"discord.js": "^14.14.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"ffmpeg": "^0.0.4",
|
"ffmpeg": "^0.0.4",
|
||||||
"humanize-duration": "^3.31.0",
|
"humanize-duration": "^3.31.0",
|
||||||
"music-metadata": "^7.14.0",
|
"music-metadata": "^7.14.0",
|
||||||
|
"similarity": "^1.2.1",
|
||||||
"sodium-native": "^4.1.1",
|
"sodium-native": "^4.1.1",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.4.3",
|
||||||
"utf-8-validate": "^6.0.3",
|
"utf-8-validate": "^6.0.3",
|
||||||
@ -38,8 +40,10 @@
|
|||||||
"@babel/preset-typescript": "^7.24.1",
|
"@babel/preset-typescript": "^7.24.1",
|
||||||
"@types/babel__core": "^7",
|
"@types/babel__core": "^7",
|
||||||
"@types/babel__preset-env": "^7",
|
"@types/babel__preset-env": "^7",
|
||||||
|
"@types/common-tags": "^1",
|
||||||
"@types/eslint": "^8",
|
"@types/eslint": "^8",
|
||||||
"@types/humanize-duration": "^3",
|
"@types/humanize-duration": "^3",
|
||||||
|
"@types/similarity": "^1",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||||
"@typescript-eslint/parser": "^7.3.1",
|
"@typescript-eslint/parser": "^7.3.1",
|
||||||
"eslint": "^8.57.0"
|
"eslint": "^8.57.0"
|
||||||
|
@ -7,8 +7,8 @@ import Util from '../../utilities/Util.js';
|
|||||||
import Initialisable from '../../interfaces/Initialisable.js';
|
import Initialisable from '../../interfaces/Initialisable.js';
|
||||||
import DiscordClient from '../DiscordClient.js';
|
import DiscordClient from '../DiscordClient.js';
|
||||||
import { Collection } from 'discord.js';
|
import { Collection } from 'discord.js';
|
||||||
import { MusicIndexEntry } from '../../../@types/MusicPlayer.js';
|
import { MusicIndexEntry, MusicQuery } from '../../../@types/MusicPlayer.js';
|
||||||
|
import similarity from 'similarity';
|
||||||
|
|
||||||
class MusicLibrary implements Initialisable
|
class MusicLibrary implements Initialisable
|
||||||
{
|
{
|
||||||
@ -47,7 +47,43 @@ class MusicLibrary implements Initialisable
|
|||||||
|
|
||||||
stop (): void | Promise<void>
|
stop (): void | Promise<void>
|
||||||
{
|
{
|
||||||
throw new Error('Method not implemented.');
|
this.saveIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
search (query: MusicQuery)
|
||||||
|
{
|
||||||
|
if (!Object.keys(query).length)
|
||||||
|
throw new Error('Invalid query');
|
||||||
|
const results: MusicIndexEntry[] = [];
|
||||||
|
for (const entry of this.#index.values())
|
||||||
|
{
|
||||||
|
if (query.artist && !entry.arist.toLowerCase().includes(query.artist.toLowerCase()))
|
||||||
|
continue;
|
||||||
|
if (query.title && !entry.title.toLowerCase().includes(query.title.toLowerCase()))
|
||||||
|
continue;
|
||||||
|
if (query.keyword
|
||||||
|
&& !entry.title.toLowerCase().includes(query.keyword?.toLowerCase())
|
||||||
|
&& !entry.arist.toLowerCase().includes(query.keyword.toLowerCase()))
|
||||||
|
continue;
|
||||||
|
results.push(entry);
|
||||||
|
}
|
||||||
|
return results.sort((a, b) =>
|
||||||
|
{
|
||||||
|
if (query.artist)
|
||||||
|
{
|
||||||
|
if (similarity(a.arist, query.artist) > similarity(b.arist, query.artist))
|
||||||
|
return 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (query.title)
|
||||||
|
{
|
||||||
|
if (similarity(a.title, query.title) > similarity(b.title, query.title))
|
||||||
|
return 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getRandom ()
|
getRandom ()
|
||||||
@ -61,6 +97,18 @@ class MusicLibrary implements Initialisable
|
|||||||
return Util.shuffle(entries);
|
return Util.shuffle(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countPlay (fp: string)
|
||||||
|
{
|
||||||
|
if (this.#index.has(fp))
|
||||||
|
this.#index.get(fp)!.stats.plays++;
|
||||||
|
}
|
||||||
|
|
||||||
|
countSkip (fp: string)
|
||||||
|
{
|
||||||
|
if (this.#index.has(fp))
|
||||||
|
this.#index.get(fp)!.stats.skips++;
|
||||||
|
}
|
||||||
|
|
||||||
async scanLibrary (full = false)
|
async scanLibrary (full = false)
|
||||||
{
|
{
|
||||||
this.#logger.info('Starting library scan');
|
this.#logger.info('Starting library scan');
|
||||||
@ -95,13 +143,17 @@ class MusicLibrary implements Initialisable
|
|||||||
title: common.title ?? title,
|
title: common.title ?? title,
|
||||||
album: common.album,
|
album: common.album,
|
||||||
year: common.year,
|
year: common.year,
|
||||||
file: fp
|
file: fp,
|
||||||
|
stats: {
|
||||||
|
plays: 0,
|
||||||
|
skips: 0
|
||||||
|
}
|
||||||
};
|
};
|
||||||
this.#index.set(fp, entry);
|
this.#index.set(fp, entry);
|
||||||
}
|
}
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
const newFiles = this.#index.size - initialSize;
|
const newFiles = this.#index.size - initialSize;
|
||||||
this.#logger.info(`Library scan took ${end - start} ms, ${this.#index.size} (new ${newFiles}) files indexed`);
|
this.#logger.info(`Library scan took ${end - start} ms, ${this.#index.size} (${newFiles} new) files indexed`);
|
||||||
this.saveIndex();
|
this.saveIndex();
|
||||||
return newFiles;
|
return newFiles;
|
||||||
}
|
}
|
||||||
@ -115,7 +167,12 @@ class MusicLibrary implements Initialisable
|
|||||||
const raw = fs.readFileSync(indexPath, { encoding: 'utf-8' });
|
const raw = fs.readFileSync(indexPath, { encoding: 'utf-8' });
|
||||||
const parsed = JSON.parse(raw) as MusicIndexEntry[];
|
const parsed = JSON.parse(raw) as MusicIndexEntry[];
|
||||||
for (const entry of parsed)
|
for (const entry of parsed)
|
||||||
|
{
|
||||||
|
if (typeof entry.stats === 'undefined')
|
||||||
|
entry.stats = { plays: 0, skips: 0 };
|
||||||
this.#index.set(entry.file, entry);
|
this.#index.set(entry.file, entry);
|
||||||
|
}
|
||||||
|
this.#logger.info(`Index loaded with ${this.#index.size} entries`);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveIndex ()
|
saveIndex ()
|
||||||
|
@ -4,7 +4,7 @@ import { LoggerClient } from '@navy.gif/logger';
|
|||||||
|
|
||||||
import Initialisable from '../../interfaces/Initialisable.js';
|
import Initialisable from '../../interfaces/Initialisable.js';
|
||||||
import DiscordClient from '../DiscordClient.js';
|
import DiscordClient from '../DiscordClient.js';
|
||||||
import { MusicIndexEntry, MusicPlayerOptions } from '../../../@types/MusicPlayer.js';
|
import { MusicIndexEntry, MusicPlayerOptions, QueueOrder } from '../../../@types/MusicPlayer.js';
|
||||||
import MusicLibrary from './MusicLibrary.js';
|
import MusicLibrary from './MusicLibrary.js';
|
||||||
|
|
||||||
type ConnectionDetails = {
|
type ConnectionDetails = {
|
||||||
@ -27,9 +27,13 @@ class MusicPlayer implements Initialisable
|
|||||||
#options: MusicPlayerOptions;
|
#options: MusicPlayerOptions;
|
||||||
#volume: number;
|
#volume: number;
|
||||||
#library: MusicLibrary;
|
#library: MusicLibrary;
|
||||||
|
|
||||||
#shuffleIdx: number;
|
#shuffleIdx: number;
|
||||||
#shuffleList: MusicIndexEntry[];
|
#shuffleList: MusicIndexEntry[];
|
||||||
|
|
||||||
|
#queue: MusicIndexEntry[];
|
||||||
|
#currentSong: MusicIndexEntry | null;
|
||||||
|
|
||||||
constructor (client: DiscordClient, options: MusicPlayerOptions)
|
constructor (client: DiscordClient, options: MusicPlayerOptions)
|
||||||
{
|
{
|
||||||
this.#client = client;
|
this.#client = client;
|
||||||
@ -45,6 +49,8 @@ class MusicPlayer implements Initialisable
|
|||||||
this.#library = new MusicLibrary(client, this.#options.library);
|
this.#library = new MusicLibrary(client, this.#options.library);
|
||||||
this.#shuffleList = [];
|
this.#shuffleList = [];
|
||||||
this.#shuffleIdx = 0;
|
this.#shuffleIdx = 0;
|
||||||
|
this.#queue = [];
|
||||||
|
this.#currentSong = null;
|
||||||
|
|
||||||
this.#player.on(AudioPlayerStatus.Idle, this.playNext.bind(this));
|
this.#player.on(AudioPlayerStatus.Idle, this.playNext.bind(this));
|
||||||
}
|
}
|
||||||
@ -59,10 +65,30 @@ class MusicPlayer implements Initialisable
|
|||||||
return this.#library;
|
return this.#library;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queue (order: QueueOrder)
|
||||||
|
{
|
||||||
|
const [ result ] = this.library.search(order);
|
||||||
|
if (!result)
|
||||||
|
return null;
|
||||||
|
this.#queue.push(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
playNext ()
|
playNext ()
|
||||||
{
|
{
|
||||||
const info = this.#shuffleList[this.#shuffleIdx];
|
if (!this.#ready)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.#player.state.status !== AudioPlayerStatus.Idle && this.#currentSong)
|
||||||
|
this.library.countSkip(this.#currentSong.file);
|
||||||
|
|
||||||
|
let info: MusicIndexEntry | null = null;
|
||||||
|
if (this.#queue.length)
|
||||||
|
info = this.#queue.shift()!;
|
||||||
|
else
|
||||||
|
info = this.#shuffleList[this.#shuffleIdx];
|
||||||
|
|
||||||
|
this.#currentSong = info;
|
||||||
this.#currentResource = createAudioResource(info.file, {
|
this.#currentResource = createAudioResource(info.file, {
|
||||||
inlineVolume: true,
|
inlineVolume: true,
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -73,11 +99,16 @@ class MusicPlayer implements Initialisable
|
|||||||
|
|
||||||
this.#logger.info(`Now playing ${info.arist} - ${info.title}`);
|
this.#logger.info(`Now playing ${info.arist} - ${info.title}`);
|
||||||
this.#player.play(this.#currentResource);
|
this.#player.play(this.#currentResource);
|
||||||
|
this.#library.countPlay(info.file);
|
||||||
|
|
||||||
this.#shuffleIdx++;
|
this.#shuffleIdx++;
|
||||||
|
if (this.#shuffleIdx === this.#shuffleList.length)
|
||||||
|
this.#shuffleIdx = 0;
|
||||||
|
|
||||||
this.#client.user?.setPresence({
|
this.#client.user?.setPresence({
|
||||||
activities: [{
|
activities: [{
|
||||||
name: `${info.arist} - ${info.title}`,
|
name: `${info.arist} - ${info.title}`,
|
||||||
type: ActivityType.Playing
|
type: ActivityType.Listening
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -92,7 +123,7 @@ class MusicPlayer implements Initialisable
|
|||||||
await channel.send({
|
await channel.send({
|
||||||
embeds: [{
|
embeds: [{
|
||||||
title: 'Now playing :notes:',
|
title: 'Now playing :notes:',
|
||||||
description: `**${info.title}** by ${info.arist}`,
|
description: `**${info!.title}** by ${info!.arist}`,
|
||||||
color: 0xffafff
|
color: 0xffafff
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@ -119,6 +150,8 @@ class MusicPlayer implements Initialisable
|
|||||||
|
|
||||||
stop (): void | Promise<void>
|
stop (): void | Promise<void>
|
||||||
{
|
{
|
||||||
|
this.#ready = false;
|
||||||
|
this.#logger.info('Stopping music player');
|
||||||
this.#logger.info('Disconnecting all guilds');
|
this.#logger.info('Disconnecting all guilds');
|
||||||
for (const [ guildId, { connection, subscription }] of this.#connections)
|
for (const [ guildId, { connection, subscription }] of this.#connections)
|
||||||
{
|
{
|
||||||
@ -126,6 +159,7 @@ class MusicPlayer implements Initialisable
|
|||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
}
|
}
|
||||||
|
this.#library.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialise ()
|
async initialise ()
|
||||||
@ -140,6 +174,7 @@ class MusicPlayer implements Initialisable
|
|||||||
|
|
||||||
this.initialiseVoiceChannels();
|
this.initialiseVoiceChannels();
|
||||||
|
|
||||||
|
this.#ready = true;
|
||||||
this.playNext();
|
this.playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
40
src/client/components/commands/Queue.ts
Normal file
40
src/client/components/commands/Queue.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Message } from 'discord.js';
|
||||||
|
import Command from '../../../interfaces/Command.js';
|
||||||
|
import DiscordClient from '../../DiscordClient.js';
|
||||||
|
import { CommandOpts } from '@navy.gif/commandparser';
|
||||||
|
|
||||||
|
class QueueCommand extends Command
|
||||||
|
{
|
||||||
|
constructor (client: DiscordClient)
|
||||||
|
{
|
||||||
|
super(client, {
|
||||||
|
name: 'queue',
|
||||||
|
showUsage: true,
|
||||||
|
options: [{
|
||||||
|
name: 'artist',
|
||||||
|
flag: true
|
||||||
|
}, {
|
||||||
|
name: 'song',
|
||||||
|
required: true
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute (message: Message<true>, { args }: CommandOpts)
|
||||||
|
{
|
||||||
|
const { member, guild } = message;
|
||||||
|
const { me } = guild.members;
|
||||||
|
if (!member?.voice || member.voice.channelId !== me?.voice.channelId)
|
||||||
|
return 'Only vc participants can queue songs';
|
||||||
|
const query = {
|
||||||
|
title: args.song!.value as string,
|
||||||
|
artist: args.artist?.value as string | undefined,
|
||||||
|
};
|
||||||
|
const result = this.client.musicPlayer.queue(query);
|
||||||
|
if (!result)
|
||||||
|
return 'Query yielded no results';
|
||||||
|
return `Song **${result.title}** by ${result.arist} queued`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueCommand;
|
@ -13,7 +13,7 @@ class PingCommand extends Command
|
|||||||
|
|
||||||
async execute ()
|
async execute ()
|
||||||
{
|
{
|
||||||
const diff = this.client.musicPlayer.library.scanLibrary();
|
const diff = await this.client.musicPlayer.library.scanLibrary();
|
||||||
const songs = this.client.musicPlayer.library.size;
|
const songs = this.client.musicPlayer.library.size;
|
||||||
return `Found ${songs} tracks with ${diff} new ones`;
|
return `Found ${songs} tracks with ${diff} new ones`;
|
||||||
}
|
}
|
||||||
|
42
src/client/components/commands/Search.ts
Normal file
42
src/client/components/commands/Search.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Message } from 'discord.js';
|
||||||
|
import Command from '../../../interfaces/Command.js';
|
||||||
|
import DiscordClient from '../../DiscordClient.js';
|
||||||
|
import { CommandOpts } from '@navy.gif/commandparser';
|
||||||
|
|
||||||
|
class SearchCommand extends Command
|
||||||
|
{
|
||||||
|
constructor (client: DiscordClient)
|
||||||
|
{
|
||||||
|
super(client, {
|
||||||
|
name: 'search',
|
||||||
|
showUsage: true,
|
||||||
|
options: [{
|
||||||
|
name: 'keyword',
|
||||||
|
}, {
|
||||||
|
name: 'artist',
|
||||||
|
flag: true
|
||||||
|
}, {
|
||||||
|
name: 'song',
|
||||||
|
flag: true
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute (_message: Message, { args }: CommandOpts)
|
||||||
|
{
|
||||||
|
const query = {
|
||||||
|
title: args.song?.value as string | undefined,
|
||||||
|
artist: args.artist?.value as string | undefined,
|
||||||
|
keyword: args.keyword?.value as string | undefined
|
||||||
|
};
|
||||||
|
const results = this.client.musicPlayer.library.search(query);
|
||||||
|
|
||||||
|
if (!results.length)
|
||||||
|
return 'No results found';
|
||||||
|
return `
|
||||||
|
**Search results:**\n${results.map(result => `\t\\- **${result.arist}** - ${result.title} (${result.album ?? 'Unknown album'}) [${result.year ?? 'Unknown year'}]`).join('\n')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchCommand;
|
41
src/client/components/commands/SetAvatar.ts
Normal file
41
src/client/components/commands/SetAvatar.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ArgsResult, CommandOpts, OptionType } from '@navy.gif/commandparser';
|
||||||
|
import Command from '../../../interfaces/Command.js';
|
||||||
|
import DiscordClient from '../../DiscordClient.js';
|
||||||
|
import { Message } from 'discord.js';
|
||||||
|
|
||||||
|
class SetAvatarCommand extends Command
|
||||||
|
{
|
||||||
|
constructor (client: DiscordClient)
|
||||||
|
{
|
||||||
|
super(client, {
|
||||||
|
name: 'set',
|
||||||
|
options: [{
|
||||||
|
name: 'avatar',
|
||||||
|
type: OptionType.SUB_COMMAND,
|
||||||
|
options: [{
|
||||||
|
name: 'asset',
|
||||||
|
required: true
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
restricted: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute (message: Message, { subcommand, args }: CommandOpts)
|
||||||
|
{
|
||||||
|
if (subcommand === 'avatar')
|
||||||
|
return this.#setAvatar(message, args);
|
||||||
|
return 'Unknown subcommand';
|
||||||
|
}
|
||||||
|
|
||||||
|
async #setAvatar (_message: Message, args : ArgsResult)
|
||||||
|
{
|
||||||
|
const { asset } = args;
|
||||||
|
if (!asset?.value)
|
||||||
|
return 'Missing value';
|
||||||
|
await this.client.user?.setAvatar(asset.value as string);
|
||||||
|
return 'Avatar successfully set';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SetAvatarCommand;
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Message } from 'discord.js';
|
||||||
import Command from '../../../interfaces/Command.js';
|
import Command from '../../../interfaces/Command.js';
|
||||||
import DiscordClient from '../../DiscordClient.js';
|
import DiscordClient from '../../DiscordClient.js';
|
||||||
|
|
||||||
@ -7,12 +8,19 @@ class SkipCommand extends Command
|
|||||||
{
|
{
|
||||||
super(client, {
|
super(client, {
|
||||||
name: 'skip',
|
name: 'skip',
|
||||||
guildOnly: true
|
guildOnly: true,
|
||||||
|
restricted: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute ()
|
async execute (message: Message<true>)
|
||||||
{
|
{
|
||||||
|
const { member, author, guild } = message;
|
||||||
|
const { me } = guild.members;
|
||||||
|
if (!member?.voice || member.voice.channelId !== me?.voice.channelId)
|
||||||
|
return 'Only vc participants can adjust volume';
|
||||||
|
|
||||||
|
this.logger.info(`${author.username} (${author.id}) skipped a song`);
|
||||||
this.client.musicPlayer.playNext();
|
this.client.musicPlayer.playNext();
|
||||||
return 'Song skipped';
|
return 'Song skipped';
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ class VolumeCommand extends Command
|
|||||||
{
|
{
|
||||||
super(client, {
|
super(client, {
|
||||||
name: 'volume',
|
name: 'volume',
|
||||||
aliases: [ 'v' ],
|
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'volume',
|
name: 'volume',
|
||||||
@ -19,7 +18,8 @@ class VolumeCommand extends Command
|
|||||||
maximum: 100
|
maximum: 100
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
guildOnly: true
|
guildOnly: true,
|
||||||
|
restricted: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { inspect } from 'node:util';
|
import { inspect } from 'node:util';
|
||||||
|
|
||||||
import { APIEmbed, ChannelType, DiscordAPIError, EmbedBuilder, Events, Message, MessagePayload } from 'discord.js';
|
import { APIEmbed, ChannelType, DiscordAPIError, EmbedBuilder, Events, Message, MessagePayload } from 'discord.js';
|
||||||
import { CommandOpts, ICommand, Parser, ParserError } from '@navy.gif/commandparser';
|
import { CommandOpts, ICommand, OptionType, Parser, ParserError } from '@navy.gif/commandparser';
|
||||||
|
|
||||||
import Observer from '../../../interfaces/Observer.js';
|
import Observer from '../../../interfaces/Observer.js';
|
||||||
import DiscordClient from '../../DiscordClient.js';
|
import DiscordClient from '../../DiscordClient.js';
|
||||||
@ -9,6 +9,7 @@ import { InhibitorResponse } from '../../../../@types/DiscordClient.js';
|
|||||||
import Command from '../../../interfaces/Command.js';
|
import Command from '../../../interfaces/Command.js';
|
||||||
import CommandError from '../../../errors/CommandError.js';
|
import CommandError from '../../../errors/CommandError.js';
|
||||||
import Util from '../../../utilities/Util.js';
|
import Util from '../../../utilities/Util.js';
|
||||||
|
import { stripIndents } from 'common-tags';
|
||||||
|
|
||||||
|
|
||||||
class CommandHandler extends Observer
|
class CommandHandler extends Observer
|
||||||
@ -119,6 +120,9 @@ class CommandHandler extends Observer
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((command as Command).showUsage && !Object.keys(rest.args).length && !rest.subcommand && !rest.subcommandGroup)
|
||||||
|
return this.#showUsage(message, command as Command);
|
||||||
|
|
||||||
this.#executeCommand(message, command, rest);
|
this.#executeCommand(message, command, rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,6 +224,34 @@ class CommandHandler extends Observer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#showUsage (message: Message<boolean>, command: Command): void | PromiseLike<void>
|
||||||
|
{
|
||||||
|
const { options } = command;
|
||||||
|
const flags = options.filter(option => option.flag);
|
||||||
|
const nonFlags = options.filter(option => !option.flag);
|
||||||
|
let output = stripIndents`
|
||||||
|
USAGE: \`${this.client.prefix}${command.name} [OPTIONS] [FLAGS]\`
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (nonFlags.length)
|
||||||
|
{
|
||||||
|
output += '\n\n' + stripIndents`
|
||||||
|
OPTIONS:
|
||||||
|
${nonFlags.map(opt => `\t \\- ${opt.name} (${OptionType[opt.type]})`).join('\n')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.length)
|
||||||
|
{
|
||||||
|
output += '\n\n' + stripIndents`
|
||||||
|
FLAGS:
|
||||||
|
${flags.map(flag => `\t \\- ${flag.name} (${OptionType[flag.type]})`).join('\n')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.reply(output);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommandHandler;
|
export default CommandHandler;
|
@ -18,6 +18,7 @@ abstract class Command extends Component implements ICommand
|
|||||||
#guildOnly: boolean;
|
#guildOnly: boolean;
|
||||||
#dmOnly: boolean;
|
#dmOnly: boolean;
|
||||||
#limited: Snowflake[] | null; // Limited to specific roles
|
#limited: Snowflake[] | null; // Limited to specific roles
|
||||||
|
#showUsage: boolean;
|
||||||
|
|
||||||
constructor (client: DiscordClient, def: CommandDefinition)
|
constructor (client: DiscordClient, def: CommandDefinition)
|
||||||
{
|
{
|
||||||
@ -33,6 +34,7 @@ abstract class Command extends Component implements ICommand
|
|||||||
this.#guildOnly = def.guildOnly ?? false;
|
this.#guildOnly = def.guildOnly ?? false;
|
||||||
this.#dmOnly = def.dmOnly ?? false;
|
this.#dmOnly = def.dmOnly ?? false;
|
||||||
this.#limited = def.limited ?? null;
|
this.#limited = def.limited ?? null;
|
||||||
|
this.#showUsage = def.showUsage ?? false;
|
||||||
|
|
||||||
this.#options = [];
|
this.#options = [];
|
||||||
|
|
||||||
@ -67,6 +69,11 @@ abstract class Command extends Component implements ICommand
|
|||||||
throw new CommandError(this, { reason: 'Command timed out', user });
|
throw new CommandError(this, { reason: 'Command timed out', user });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showUsage ()
|
||||||
|
{
|
||||||
|
return this.#showUsage;
|
||||||
|
}
|
||||||
|
|
||||||
get restricted ()
|
get restricted ()
|
||||||
{
|
{
|
||||||
return this.#restricted;
|
return this.#restricted;
|
||||||
|
45
yarn.lock
45
yarn.lock
@ -1804,6 +1804,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/common-tags@npm:^1":
|
||||||
|
version: 1.8.4
|
||||||
|
resolution: "@types/common-tags@npm:1.8.4"
|
||||||
|
checksum: 10/40c95a2f6388beb1cdeed3c9986ac0d6a3a551fce706e3e364a00ded48ab624b06b1ac8b94679bb2da9653e5eb3e450bad26873f5189993a5d8e8bdace74cbb2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/eslint@npm:^8":
|
"@types/eslint@npm:^8":
|
||||||
version: 8.56.6
|
version: 8.56.6
|
||||||
resolution: "@types/eslint@npm:8.56.6"
|
resolution: "@types/eslint@npm:8.56.6"
|
||||||
@ -1851,6 +1858,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/similarity@npm:^1":
|
||||||
|
version: 1.2.3
|
||||||
|
resolution: "@types/similarity@npm:1.2.3"
|
||||||
|
checksum: 10/1f3c9ad6e803e3c1d161c6701da09686a86a8772703765b259d3614ab1b0a84294b0b9f9044d596f7310cf583cc8614e1b1699f3e22175ba4af01fd3b295804e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/ws@npm:8.5.9":
|
"@types/ws@npm:8.5.9":
|
||||||
version: 8.5.9
|
version: 8.5.9
|
||||||
resolution: "@types/ws@npm:8.5.9"
|
resolution: "@types/ws@npm:8.5.9"
|
||||||
@ -2353,6 +2367,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"common-tags@npm:^1.8.2":
|
||||||
|
version: 1.8.2
|
||||||
|
resolution: "common-tags@npm:1.8.2"
|
||||||
|
checksum: 10/c665d0f463ee79dda801471ad8da6cb33ff7332ba45609916a508ad3d77ba07ca9deeb452e83f81f24c2b081e2c1315347f23d239210e63d1c5e1a0c7c019fe2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"concat-map@npm:0.0.1":
|
"concat-map@npm:0.0.1":
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
resolution: "concat-map@npm:0.0.1"
|
resolution: "concat-map@npm:0.0.1"
|
||||||
@ -3251,6 +3272,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"levenshtein-edit-distance@npm:^2.0.0":
|
||||||
|
version: 2.0.5
|
||||||
|
resolution: "levenshtein-edit-distance@npm:2.0.5"
|
||||||
|
bin:
|
||||||
|
levenshtein-edit-distance: cli.js
|
||||||
|
checksum: 10/50618c01cd0c9bae6d4371d75af62c17c25a8f91bfd8d06400315b8b15976900cff951b48e102e074e9c5c6758260fff1675cfad186732afe124a5708e1032fd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"levn@npm:^0.4.1":
|
"levn@npm:^0.4.1":
|
||||||
version: 0.4.1
|
version: 0.4.1
|
||||||
resolution: "levn@npm:0.4.1"
|
resolution: "levn@npm:0.4.1"
|
||||||
@ -3552,17 +3582,21 @@ __metadata:
|
|||||||
"@navy.gif/timestring": "npm:^6.0.6"
|
"@navy.gif/timestring": "npm:^6.0.6"
|
||||||
"@types/babel__core": "npm:^7"
|
"@types/babel__core": "npm:^7"
|
||||||
"@types/babel__preset-env": "npm:^7"
|
"@types/babel__preset-env": "npm:^7"
|
||||||
|
"@types/common-tags": "npm:^1"
|
||||||
"@types/eslint": "npm:^8"
|
"@types/eslint": "npm:^8"
|
||||||
"@types/humanize-duration": "npm:^3"
|
"@types/humanize-duration": "npm:^3"
|
||||||
|
"@types/similarity": "npm:^1"
|
||||||
"@typescript-eslint/eslint-plugin": "npm:^7.3.1"
|
"@typescript-eslint/eslint-plugin": "npm:^7.3.1"
|
||||||
"@typescript-eslint/parser": "npm:^7.3.1"
|
"@typescript-eslint/parser": "npm:^7.3.1"
|
||||||
bufferutil: "npm:^4.0.8"
|
bufferutil: "npm:^4.0.8"
|
||||||
|
common-tags: "npm:^1.8.2"
|
||||||
discord.js: "npm:^14.14.1"
|
discord.js: "npm:^14.14.1"
|
||||||
dotenv: "npm:^16.4.5"
|
dotenv: "npm:^16.4.5"
|
||||||
eslint: "npm:^8.57.0"
|
eslint: "npm:^8.57.0"
|
||||||
ffmpeg: "npm:^0.0.4"
|
ffmpeg: "npm:^0.0.4"
|
||||||
humanize-duration: "npm:^3.31.0"
|
humanize-duration: "npm:^3.31.0"
|
||||||
music-metadata: "npm:^7.14.0"
|
music-metadata: "npm:^7.14.0"
|
||||||
|
similarity: "npm:^1.2.1"
|
||||||
sodium-native: "npm:^4.1.1"
|
sodium-native: "npm:^4.1.1"
|
||||||
typescript: "npm:^5.4.3"
|
typescript: "npm:^5.4.3"
|
||||||
utf-8-validate: "npm:^6.0.3"
|
utf-8-validate: "npm:^6.0.3"
|
||||||
@ -4078,6 +4112,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"similarity@npm:^1.2.1":
|
||||||
|
version: 1.2.1
|
||||||
|
resolution: "similarity@npm:1.2.1"
|
||||||
|
dependencies:
|
||||||
|
levenshtein-edit-distance: "npm:^2.0.0"
|
||||||
|
bin:
|
||||||
|
similarity: cli.js
|
||||||
|
checksum: 10/7010abfb53ea72fcecb3b9f59e0753f589256b8a134886a5282cd1ee21226fdc7f11bfe45f673631928d8ae88283f23e6b6f5c4e84ab3f15d175b2d520e1bffe
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"slash@npm:^3.0.0":
|
"slash@npm:^3.0.0":
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
resolution: "slash@npm:3.0.0"
|
resolution: "slash@npm:3.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user