2022-03-25 00:19:48 +01:00
import { Client , Collection , ExcludeEnum , PresenceStatusData , User } from 'discord.js' ;
2020-03-13 04:41:26 +01:00
import { inject , injectable } from 'inversify' ;
2021-12-14 02:11:14 +01:00
import ora from 'ora' ;
2021-09-20 04:04:34 +02:00
import { TYPES } from './types.js' ;
import container from './inversify.config.js' ;
import Command from './commands/index.js' ;
import debug from './utils/debug.js' ;
import handleGuildCreate from './events/guild-create.js' ;
import handleVoiceStateUpdate from './events/voice-state-update.js' ;
2022-02-05 23:16:17 +01:00
import handleGuildUpdate from './events/guild-update.js' ;
2021-09-20 04:04:34 +02:00
import errorMsg from './utils/error-msg.js' ;
import { isUserInVoice } from './utils/channels.js' ;
import Config from './services/config.js' ;
2021-11-12 20:30:18 +01:00
import { generateDependencyReport } from '@discordjs/voice' ;
2022-02-05 23:16:17 +01:00
import { REST } from '@discordjs/rest' ;
import { Routes } from 'discord-api-types/v9' ;
import updatePermissionsForGuild from './utils/update-permissions-for-guild.js' ;
2022-03-25 00:19:48 +01:00
import { ActivityTypes } from 'discord.js/typings/enums' ;
2020-03-13 04:41:26 +01:00
@injectable ( )
export default class {
private readonly client : Client ;
2022-03-25 00:19:48 +01:00
private readonly config : Config ;
2020-03-13 04:41:26 +01:00
private readonly token : string ;
2022-02-05 23:16:17 +01:00
private readonly shouldRegisterCommandsOnBot : boolean ;
private readonly commandsByName ! : Collection < string , Command > ;
private readonly commandsByButtonId ! : Collection < string , Command > ;
2020-03-13 04:41:26 +01:00
2022-01-05 21:30:32 +01:00
constructor (
@inject ( TYPES . Client ) client : Client ,
@inject ( TYPES . Config ) config : Config ,
) {
2020-03-13 04:41:26 +01:00
this . client = client ;
2022-03-25 00:19:48 +01:00
this . config = config ;
2021-09-20 01:50:25 +02:00
this . token = config . DISCORD_TOKEN ;
2022-02-05 23:16:17 +01:00
this . shouldRegisterCommandsOnBot = config . REGISTER_COMMANDS_ON_BOT ;
this . commandsByName = new Collection ( ) ;
this . commandsByButtonId = new Collection ( ) ;
2020-03-13 04:41:26 +01:00
}
2022-02-05 23:16:17 +01:00
public async register ( ) : Promise < void > {
2020-03-13 04:41:26 +01:00
// Load in commands
2022-02-05 23:16:17 +01:00
for ( const command of container . getAll < Command > ( TYPES . Command ) ) {
// Make sure we can serialize to JSON without errors
try {
command . slashCommand . toJSON ( ) ;
} catch ( error ) {
console . error ( error ) ;
throw new Error ( ` Could not serialize / ${ command . slashCommand . name ? ? '' } to JSON ` ) ;
2020-03-16 18:13:18 +01:00
}
2022-02-05 23:16:17 +01:00
if ( command . slashCommand . name ) {
this . commandsByName . set ( command . slashCommand . name , command ) ;
2020-03-13 04:41:26 +01:00
}
2022-02-05 23:16:17 +01:00
if ( command . handledButtonIds ) {
for ( const buttonId of command . handledButtonIds ) {
this . commandsByButtonId . set ( buttonId , command ) ;
2020-03-17 01:37:54 +01:00
}
2020-03-13 04:41:26 +01:00
}
2022-02-05 23:16:17 +01:00
}
2020-03-13 04:41:26 +01:00
2022-02-05 23:16:17 +01:00
// Register event handlers
this . client . on ( 'interactionCreate' , async interaction = > {
2020-03-13 04:41:26 +01:00
try {
2022-02-05 23:16:17 +01:00
if ( interaction . isCommand ( ) ) {
const command = this . commandsByName . get ( interaction . commandName ) ;
if ( ! command ) {
return ;
}
if ( ! interaction . guild ) {
await interaction . reply ( errorMsg ( 'you can\'t use this bot in a DM' ) ) ;
return ;
}
const requiresVC = command . requiresVC instanceof Function ? command . requiresVC ( interaction ) : command . requiresVC ;
if ( requiresVC && interaction . member && ! isUserInVoice ( interaction . guild , interaction . member . user as User ) ) {
await interaction . reply ( { content : errorMsg ( 'gotta be in a voice channel' ) , ephemeral : true } ) ;
return ;
}
if ( command . execute ) {
await command . execute ( interaction ) ;
}
} else if ( interaction . isButton ( ) ) {
const command = this . commandsByButtonId . get ( interaction . customId ) ;
if ( ! command ) {
return ;
}
if ( command . handleButtonInteraction ) {
await command . handleButtonInteraction ( interaction ) ;
}
} else if ( interaction . isAutocomplete ( ) ) {
const command = this . commandsByName . get ( interaction . commandName ) ;
if ( ! command ) {
return ;
}
if ( command . handleAutocompleteInteraction ) {
await command . handleAutocompleteInteraction ( interaction ) ;
}
2020-03-19 23:39:55 +01:00
}
2020-10-24 18:32:43 +02:00
} catch ( error : unknown ) {
2020-03-21 02:47:04 +01:00
debug ( error ) ;
2022-02-05 23:16:17 +01:00
// This can fail if the message was deleted, and we don't want to crash the whole bot
try {
if ( ( interaction . isApplicationCommand ( ) || interaction . isButton ( ) ) && ( interaction . replied || interaction . deferred ) ) {
await interaction . editReply ( errorMsg ( error as Error ) ) ;
} else if ( interaction . isApplicationCommand ( ) || interaction . isButton ( ) ) {
await interaction . reply ( { content : errorMsg ( error as Error ) , ephemeral : true } ) ;
}
} catch { }
2020-03-13 04:41:26 +01:00
}
} ) ;
2021-12-14 02:11:14 +01:00
const spinner = ora ( '📡 connecting to Discord...' ) . start ( ) ;
2022-02-05 23:16:17 +01:00
this . client . once ( 'ready' , async ( ) = > {
2021-11-12 20:30:18 +01:00
debug ( generateDependencyReport ( ) ) ;
2021-12-14 02:11:14 +01:00
2022-02-05 23:16:17 +01:00
// Update commands
const rest = new REST ( { version : '9' } ) . setToken ( this . token ) ;
if ( this . shouldRegisterCommandsOnBot ) {
spinner . text = '📡 updating commands on bot...' ;
await rest . put (
Routes . applicationCommands ( this . client . user ! . id ) ,
{ body : this.commandsByName.map ( command = > command . slashCommand . toJSON ( ) ) } ,
) ;
} else {
spinner . text = '📡 updating commands in all guilds...' ;
await Promise . all ( [
. . . this . client . guilds . cache . map ( async guild = > {
await rest . put (
Routes . applicationGuildCommands ( this . client . user ! . id , guild . id ) ,
{ body : this.commandsByName.map ( command = > command . slashCommand . toJSON ( ) ) } ,
) ;
} ) ,
// Remove commands registered on bot (if they exist)
rest . put ( Routes . applicationCommands ( this . client . user ! . id ) , { body : [ ] } ) ,
] ,
) ;
}
2022-03-25 00:19:48 +01:00
this . client . user ! . setPresence ( {
activities : [
{
name : this.config.BOT_ACTIVITY ,
type : this . config . BOT_ACTIVITY_TYPE as unknown as ExcludeEnum < typeof ActivityTypes , ' CUSTOM ' > ,
url : this.config.BOT_ACTIVITY_URL === '' ? undefined : this . config . BOT_ACTIVITY_URL ,
} ,
] ,
status : this.config.BOT_STATUS as PresenceStatusData ,
} ) ;
2022-02-05 23:16:17 +01:00
// Update permissions
spinner . text = '📡 updating permissions...' ;
await Promise . all ( this . client . guilds . cache . map ( async guild = > updatePermissionsForGuild ( guild ) ) ) ;
2022-03-13 00:41:53 +01:00
spinner . succeed ( ` Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id= ${ this . client . user ? . id ? ? '' } &scope=bot%20applications.commands&permissions=36700288 ` ) ;
2020-03-13 04:41:26 +01:00
} ) ;
2020-03-17 02:14:15 +01:00
this . client . on ( 'error' , console . error ) ;
2020-03-17 23:59:26 +01:00
this . client . on ( 'debug' , debug ) ;
2020-03-17 01:37:54 +01:00
2020-03-13 04:41:26 +01:00
this . client . on ( 'guildCreate' , handleGuildCreate ) ;
2020-03-17 02:14:15 +01:00
this . client . on ( 'voiceStateUpdate' , handleVoiceStateUpdate ) ;
2022-02-05 23:16:17 +01:00
this . client . on ( 'guildUpdate' , handleGuildUpdate ) ;
2020-03-13 04:41:26 +01:00
2022-02-05 23:16:17 +01:00
await this . client . login ( this . token ) ;
2020-03-13 04:41:26 +01:00
}
}