Various fixes and improvements
- Added debug statements - Added option to limit commands to specific roles - Handle process exiting explicitly
This commit is contained in:
parent
1fc86a56c2
commit
2a3a034e59
21
@types/Controller.d.ts
vendored
21
@types/Controller.d.ts
vendored
@ -1,17 +1,6 @@
|
||||
import { LoggerMasterOptions } from '@navy.gif/logger';
|
||||
import { ClientOptions } from './DiscordClient.js';
|
||||
|
||||
export type ControllerOptions = {
|
||||
rootDir: string,
|
||||
logger: LoggerMasterOptions,
|
||||
shardOptions: {
|
||||
totalShards: 'auto' | number,
|
||||
shardList?: 'auto' | number[]
|
||||
respawn?: boolean,
|
||||
},
|
||||
discord: ClientOptions
|
||||
}
|
||||
|
||||
export type ShardingOptions = {
|
||||
shardList?: 'auto' | number[],
|
||||
totalShards?: 'auto' | number,
|
||||
@ -21,5 +10,13 @@ export type ShardingOptions = {
|
||||
execArgv?: string[],
|
||||
token?: string,
|
||||
path?: string,
|
||||
clientOptions?: ClientOptions
|
||||
clientOptions?: ClientOptions,
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
export type ControllerOptions = {
|
||||
rootDir: string,
|
||||
logger: LoggerMasterOptions,
|
||||
shardOptions: ShardingOptions,
|
||||
discord: ClientOptions
|
||||
}
|
3
@types/Shard.d.ts
vendored
3
@types/Shard.d.ts
vendored
@ -5,7 +5,8 @@ export type ShardOptions = {
|
||||
args?: string[];
|
||||
respawn?: boolean,
|
||||
clientOptions: ClientOptions
|
||||
totalShards: number
|
||||
totalShards: number,
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
export type ShardMethod = 'eval' | 'fetchClientValue'
|
@ -17,5 +17,5 @@ COPY build build
|
||||
COPY --from=builder /musicbot/node_modules ./node_modules
|
||||
COPY package.json package.json
|
||||
VOLUME [ "/musicbot/cache" ]
|
||||
# CMD ["node", "--enable-source-maps", "build/index.js"]
|
||||
CMD ["/bin/ash"]
|
||||
CMD ["node", "--enable-source-maps", "build/index.js"]
|
||||
# CMD ["/bin/ash"]
|
@ -68,8 +68,8 @@ class DiscordClient extends Client
|
||||
});
|
||||
|
||||
process.on('message', this.#handleMessage.bind(this));
|
||||
process.on('SIGINT', () => this.shutdown());
|
||||
process.on('SIGTERM', () => this.shutdown());
|
||||
process.on('SIGINT', () => this.#logger.info('Received SIGINT'));
|
||||
process.on('SIGTERM', () => this.#logger.info('Received SIGTERM'));
|
||||
|
||||
this.#built = false;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import similarity from 'similarity';
|
||||
import MusicDownloader from './MusicDownloader.js';
|
||||
import MusicPlayerError from '../../errors/MusicPlayerError.js';
|
||||
import { DownloaderResult } from '../../../@types/Downloader.js';
|
||||
import { inspect } from 'node:util';
|
||||
|
||||
const linkReg = /(https?:\/\/(www\.)?)?(?<domain>([a-z0-9-]{1,63}\.)?([a-z0-9-]{1,63})(\.[a-z0-9-]{2,63})(\.[a-z0-9-]{2,63})?)(\/[^()\s]*)?/iu;
|
||||
const defaultStats: MusicStatsEntry = {
|
||||
@ -91,12 +92,14 @@ class MusicLibrary implements Initialisable
|
||||
if (!domain)
|
||||
throw new MusicPlayerError('Invalid link');
|
||||
|
||||
this.#logger.debug(`Requesting download for ${keyword}`);
|
||||
let result: DownloaderResult | null = null;
|
||||
if (domain.includes('spotify.com'))
|
||||
result = await this.#downloader.download('spotify', keyword);
|
||||
else
|
||||
throw new MusicPlayerError('Unsupported domain');
|
||||
|
||||
this.#logger.debug(`Result ${inspect(result)} for keyword ${keyword}`);
|
||||
if (!result)
|
||||
return null;
|
||||
|
||||
|
@ -82,7 +82,7 @@ class MusicPlayer implements Initialisable
|
||||
this.playNext();
|
||||
}
|
||||
|
||||
stop (): void | Promise<void>
|
||||
async stop (): Promise<void>
|
||||
{
|
||||
this.#ready = false;
|
||||
this.#logger.info('Stopping music player');
|
||||
@ -91,9 +91,10 @@ class MusicPlayer implements Initialisable
|
||||
{
|
||||
this.#logger.debug(`Disconnecting ${guildId}`);
|
||||
subscription.unsubscribe();
|
||||
connection.removeAllListeners();
|
||||
connection.disconnect();
|
||||
}
|
||||
this.#library.stop();
|
||||
await this.#library.stop();
|
||||
|
||||
const config = {
|
||||
volume: this.#volume,
|
||||
@ -239,6 +240,7 @@ class MusicPlayer implements Initialisable
|
||||
guildId: guild.id,
|
||||
adapterCreator: guild.voiceAdapterCreator
|
||||
});
|
||||
connection.removeAllListeners();
|
||||
|
||||
const subscription = connection.subscribe(this.#player);
|
||||
if (!subscription)
|
||||
@ -270,6 +272,8 @@ class MusicPlayer implements Initialisable
|
||||
async #handleDisconnect (oldState: VoiceConnectionState, newState: VoiceConnectionState, guild: Guild): Promise<void>
|
||||
{
|
||||
this.#logger.debug(`Voice connection in ${guild.name} changed state from ${oldState.status} to ${newState.status}`);
|
||||
if (!this.#ready)
|
||||
return;
|
||||
const connectionData = this.#connections.get(guild.id);
|
||||
if (!connectionData)
|
||||
return;
|
||||
@ -279,17 +283,23 @@ class MusicPlayer implements Initialisable
|
||||
const { me } = guild.members;
|
||||
if (!me || !config)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await Promise.race([
|
||||
entersState(connection, VoiceConnectionStatus.Signalling, 5_000),
|
||||
entersState(connection, VoiceConnectionStatus.Connecting, 5_000)
|
||||
]);
|
||||
this.#logger.info('Connection was restored, checking if in right VC');
|
||||
// console.log(me.voice.channelId, config.voiceChannel);
|
||||
// If we find ourselves here, the bot was successfully able to reconnect to a VC
|
||||
// Now we check if it's the one it's meant to be in
|
||||
if (me.voice.channelId !== config.voiceChannel)
|
||||
{
|
||||
this.#logger.info('Moving back to designated channel');
|
||||
this.#joinVoiceChannel(guild, config.voiceChannel, config.textOutput);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.#logger.info(`Connection in ${guild.name} was terminated, attempting reconnect`);
|
||||
@ -329,6 +339,11 @@ class MusicPlayer implements Initialisable
|
||||
return this.#queue;
|
||||
}
|
||||
|
||||
get current ()
|
||||
{
|
||||
return this.#currentSong;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default MusicPlayer;
|
@ -20,7 +20,7 @@ class QueueCommand extends Command
|
||||
type: OptionType.INTEGER
|
||||
}, {
|
||||
name: 'song'
|
||||
}],
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
@ -31,12 +31,11 @@ class QueueCommand extends Command
|
||||
|
||||
if (!Object.keys(args).length)
|
||||
{
|
||||
const { queue } = this.client.musicPlayer;
|
||||
const { queue, current } = this.client.musicPlayer;
|
||||
const base = `**Now playing:** \`${current?.title} by ${current?.artist}\`\n`;
|
||||
if (!queue.length)
|
||||
return 'Queue empty';
|
||||
return `
|
||||
**Music queue:**\n\`\`\`${queue.map(entry => `\t\\- ${entry.title} by ${entry.artist}`).join('\n')}\`\`\`
|
||||
`;
|
||||
return base + '**Queue empty**';
|
||||
return base + `**Music queue:**\n\`\`\`${queue.map((entry, idx) => `\t[${idx + 1}] ${entry.title} by ${entry.artist}`).join('\n')}\`\`\``;
|
||||
}
|
||||
|
||||
if (!member?.voice || member.voice.channelId !== me?.voice.channelId)
|
||||
|
@ -10,8 +10,9 @@ class SkipCommand extends Command
|
||||
name: 'skip',
|
||||
description: 'Skips the current song.',
|
||||
guildOnly: true,
|
||||
restricted: true,
|
||||
sameVc: true
|
||||
// restricted: true,
|
||||
sameVc: true,
|
||||
limited: [ '1076274430520594514' ]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -21,8 +21,9 @@ class VolumeCommand extends Command
|
||||
}
|
||||
],
|
||||
guildOnly: true,
|
||||
restricted: true,
|
||||
// restricted: true,
|
||||
sameVc: true,
|
||||
limited: [ '1076274430520594514' ],
|
||||
});
|
||||
}
|
||||
|
||||
|
34
src/client/components/inhibitors/Limited.ts
Normal file
34
src/client/components/inhibitors/Limited.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Message } from 'discord.js';
|
||||
import Inhibitor from '../../../interfaces/Inhibitor.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
import Command from '../../../interfaces/Command.js';
|
||||
import { InhibitorResponse } from '../../../../@types/DiscordClient.js';
|
||||
import Util from '../../../utilities/Util.js';
|
||||
|
||||
class LimitedInhibitor extends Inhibitor
|
||||
{
|
||||
constructor (client: DiscordClient)
|
||||
{
|
||||
super(client, {
|
||||
name: 'Limited',
|
||||
priority: 5
|
||||
});
|
||||
}
|
||||
|
||||
override async execute (message: Message<boolean>, command: Command): Promise<InhibitorResponse>
|
||||
{
|
||||
if (!command.limited?.length || this.client.isDeveloper(message.author))
|
||||
return super._succeed();
|
||||
const { member } = message;
|
||||
if (!member)
|
||||
return super._fail('Invalid member');
|
||||
|
||||
const allowedRoles = command.limited;
|
||||
if (Util.hasAny(member.roles.cache.map(role => role.id), allowedRoles))
|
||||
return super._succeed();
|
||||
return super._fail('Missing permissions to do that');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default LimitedInhibitor;
|
@ -23,7 +23,6 @@ class Controller
|
||||
|
||||
constructor (options: ControllerOptions, version: string)
|
||||
{
|
||||
const respawn = process.env.NODE_ENV !== 'development';
|
||||
const clientPath = path.join(options.rootDir, 'client/DiscordClient.js');
|
||||
if (!fs.existsSync(clientPath))
|
||||
throw new Error(`Client path does not seem to exist: ${clientPath}`);
|
||||
@ -32,7 +31,7 @@ class Controller
|
||||
this.#shards = new Collection();
|
||||
|
||||
// this.#options = options;
|
||||
const { shardList, totalShards, execArgv } = Controller.parseShardOptions(options.shardOptions);
|
||||
const { shardList, totalShards, execArgv, respawn, debug } = Controller.parseShardOptions(options.shardOptions);
|
||||
|
||||
options.discord.rootDir = options.rootDir;
|
||||
options.discord.logger = options.logger;
|
||||
@ -46,6 +45,7 @@ class Controller
|
||||
execArgv,
|
||||
token: process.env.DISCORD_TOKEN,
|
||||
clientOptions: options.discord,
|
||||
debug
|
||||
};
|
||||
|
||||
this.#version = version;
|
||||
@ -141,15 +141,16 @@ class Controller
|
||||
|
||||
#setListeners (shard: Shard)
|
||||
{
|
||||
shard.on('death', () => this.#logger.info(`Shard ${shard.id} has died`));
|
||||
shard.on('fatal', ({ error }) => this.#logger.warn(`Shard ${shard.id} has died fatally: ${inspect(error) ?? ''}`));
|
||||
shard.on('shutdown', () => this.#logger.info(`Shard ${shard.id} is shutting down gracefully`));
|
||||
shard.on('ready', () => this.#logger.info(`Shard ${shard.id} is ready`));
|
||||
shard.on('disconnect', () => this.#logger.warn(`Shard ${shard.id} has disconnected`));
|
||||
shard.on('processDisconnect', () => this.#logger.warn(`Process for ${shard.id} has disconnected`));
|
||||
shard.on('spawn', () => this.#logger.info(`Shard ${shard.id} spawned`));
|
||||
shard.on('error', (err) => this.#logger.error(`Shard ${shard.id} ran into an error:\n${err.stack}`));
|
||||
shard.on('warn', (msg) => this.#logger.warn(`Warning from shard ${shard.id}: ${msg}`, { broadcast: true }));
|
||||
shard.on('death', () => this.#logger.info(`Shard ${shard.id} has died`))
|
||||
.on('fatal', ({ error }) => this.#logger.warn(`Shard ${shard.id} has died fatally: ${inspect(error) ?? ''}`))
|
||||
.on('shutdown', () => this.#logger.info(`Shard ${shard.id} is shutting down gracefully`))
|
||||
.on('ready', () => this.#logger.info(`Shard ${shard.id} is ready`))
|
||||
.on('disconnect', () => this.#logger.warn(`Shard ${shard.id} has disconnected`))
|
||||
.on('processDisconnect', () => this.#logger.warn(`Process for ${shard.id} has disconnected`))
|
||||
.on('spawn', () => this.#logger.info(`Shard ${shard.id} spawned`))
|
||||
.on('error', (err) => this.#logger.error(`Shard ${shard.id} ran into an error:\n${err.stack}`))
|
||||
.on('warn', (msg) => this.#logger.warn(`Warning from shard ${shard.id}: ${msg}`, { broadcast: true }))
|
||||
.on('debug', msg => this.#logger.debug(msg));
|
||||
|
||||
// shard.on('message', (msg) => this.#handleMessage(shard, msg));
|
||||
}
|
||||
@ -231,7 +232,10 @@ class Controller
|
||||
let { execArgv } = options;
|
||||
if (!execArgv)
|
||||
execArgv = [];
|
||||
return { shardList, totalShards, execArgv };
|
||||
|
||||
const respawn = (process.env.NODE_ENV !== 'development' || options.respawn) ?? false;
|
||||
|
||||
return { shardList, totalShards, execArgv, respawn, debug: options.debug ?? false };
|
||||
}
|
||||
|
||||
async shutdown (type: string)
|
||||
@ -239,16 +243,15 @@ class Controller
|
||||
if (this.#exiting)
|
||||
return;
|
||||
this.#exiting = true;
|
||||
this.#logger.info('Received SIGINT or SIGTERM, shutting down');
|
||||
this.#logger.info(`Received ${type}, shutting down`);
|
||||
setTimeout(process.exit, 90_000);
|
||||
const promises = this.#shards
|
||||
.filter(shard => shard.ready)
|
||||
.map(shard =>
|
||||
{
|
||||
if (type === 'SIGTERM')
|
||||
return shard.kill();
|
||||
return shard.awaitShutdown()
|
||||
.then(() => shard.removeAllListeners());
|
||||
// return shard.awaitShutdown()
|
||||
// .then(() => shard.removeAllListeners());
|
||||
});
|
||||
if (promises.length)
|
||||
await Promise.all(promises);
|
||||
|
@ -15,6 +15,7 @@ class Shard extends EventEmitter
|
||||
{
|
||||
[key: string]: unknown;
|
||||
|
||||
#debug: boolean;
|
||||
#id: number;
|
||||
#controller: Controller;
|
||||
#env: EnvObject; // { [key: string]: string | boolean | number };
|
||||
@ -43,6 +44,7 @@ class Shard extends EventEmitter
|
||||
super();
|
||||
this.#controller = controller;
|
||||
this.#id = id;
|
||||
this.#debug = options.debug ?? false;
|
||||
|
||||
this.#args = options.args ?? [];
|
||||
this.#execArgv = options.execArgv ?? [];
|
||||
@ -150,19 +152,19 @@ class Shard extends EventEmitter
|
||||
const onDisconnect = () =>
|
||||
{
|
||||
cleanup();
|
||||
reject(new Error(`[shard${this.id}] Shard disconnected while readying.`));
|
||||
reject(new Error(`[SHARD-${this.id}] Shard disconnected while readying.`));
|
||||
};
|
||||
|
||||
const onDeath = () =>
|
||||
{
|
||||
cleanup();
|
||||
reject(new Error(`[shard${this.id}] Shard died while readying.`));
|
||||
reject(new Error(`[SHARD-${this.id}] Shard died while readying.`));
|
||||
};
|
||||
|
||||
const onTimeout = () =>
|
||||
{
|
||||
cleanup(true);
|
||||
reject(new Error(`[shard${this.id}] Shard timed out while readying.`));
|
||||
reject(new Error(`[SHARD-${this.id}] Shard timed out while readying.`));
|
||||
};
|
||||
|
||||
const spawnTimeoutTimer = setTimeout(onTimeout, timeout);
|
||||
@ -172,7 +174,7 @@ class Shard extends EventEmitter
|
||||
});
|
||||
}
|
||||
|
||||
// When killing the process, give it an opportonity to gracefully shut down (i.e. clean up DB connections etc)
|
||||
// When killing the process, give it an opportunity to gracefully shut down (i.e. clean up DB connections etc)
|
||||
// It simply has to respond with a shutdown message to the shutdown event
|
||||
kill ()
|
||||
{
|
||||
@ -180,6 +182,7 @@ class Shard extends EventEmitter
|
||||
{
|
||||
return new Promise<void>((resolve) =>
|
||||
{
|
||||
this.debug('Shard.kill');
|
||||
if (!this.#process)
|
||||
return resolve();
|
||||
|
||||
@ -187,6 +190,7 @@ class Shard extends EventEmitter
|
||||
|
||||
const timeout = setTimeout(() =>
|
||||
{
|
||||
this.debug('Shard.kill timeout');
|
||||
if (!this.#process)
|
||||
return resolve();
|
||||
this.#process.kill();
|
||||
@ -195,6 +199,7 @@ class Shard extends EventEmitter
|
||||
|
||||
this.#process.once('exit', (code, signal) =>
|
||||
{
|
||||
this.debug('Shard.kill exit listener');
|
||||
clearTimeout(timeout);
|
||||
this.#handleExit(code, signal, false);
|
||||
resolve();
|
||||
@ -202,6 +207,7 @@ class Shard extends EventEmitter
|
||||
|
||||
this.once('shutdown', () =>
|
||||
{
|
||||
this.debug('Shard.kill shutdown listener');
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
|
||||
@ -457,6 +463,12 @@ class Shard extends EventEmitter
|
||||
if (respawn)
|
||||
this.spawn().catch((error: Error) => this.emit('error', error));
|
||||
}
|
||||
|
||||
private debug (msg: string)
|
||||
{
|
||||
if (this.#debug && this.listenerCount('debug'))
|
||||
this.emit('debug', `[SHARD-${this.id}] ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Shard;
|
@ -587,6 +587,21 @@ class Util
|
||||
return Object.keys(obj).some(key => [ 'title', 'fields', 'description', 'image', 'video' ].includes(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an array contains any values from the reference array
|
||||
* @date 4/13/2024 - 10:31:57 AM
|
||||
*
|
||||
* @static
|
||||
* @template [T=unknown]
|
||||
* @param {T[]} target
|
||||
* @param {T[]} reference
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static hasAny<T=unknown> (target: T[], reference: T[]): boolean
|
||||
{
|
||||
return target.some(entry => reference.includes(entry));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Util;
|
Loading…
Reference in New Issue
Block a user