1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
|
import {Client, Collection, User} from 'discord.js';
import {inject, injectable} from 'inversify';
import ora from 'ora';
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';
import errorMsg from './utils/error-msg.js';
import {isUserInVoice} from './utils/channels.js';
import Config from './services/config.js';
import {generateDependencyReport} from '@discordjs/voice';
import {REST} from '@discordjs/rest';
import {Routes} from 'discord-api-types/v10';
import registerCommandsOnGuild from './utils/register-commands-on-guild.js';
@injectable()
export default class {
private readonly client: Client;
private readonly config: Config;
private readonly shouldRegisterCommandsOnBot: boolean;
private readonly commandsByName!: Collection<string, Command>;
private readonly commandsByButtonId!: Collection<string, Command>;
constructor(@inject(TYPES.Client) client: Client, @inject(TYPES.Config) config: Config) {
this.client = client;
this.config = config;
this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT;
this.commandsByName = new Collection();
this.commandsByButtonId = new Collection();
}
public async register(): Promise<void> {
// Load in commands
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`);
}
if (command.slashCommand.name) {
this.commandsByName.set(command.slashCommand.name, command);
}
if (command.handledButtonIds) {
for (const buttonId of command.handledButtonIds) {
this.commandsByButtonId.set(buttonId, command);
}
}
}
// Register event handlers
// eslint-disable-next-line complexity
this.client.on('interactionCreate', async interaction => {
try {
if (interaction.isCommand()) {
const command = this.commandsByName.get(interaction.commandName);
if (!command || !interaction.isChatInputCommand()) {
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);
}
}
} catch (error: unknown) {
debug(error);
// This can fail if the message was deleted, and we don't want to crash the whole bot
try {
if ((interaction.isCommand() || interaction.isButton()) && (interaction.replied || interaction.deferred)) {
await interaction.editReply(errorMsg(error as Error));
} else if (interaction.isCommand() || interaction.isButton()) {
await interaction.reply({content: errorMsg(error as Error), ephemeral: true});
}
} catch {}
}
});
const spinner = ora('📡 connecting to Discord...').start();
this.client.once('ready', async () => {
debug(generateDependencyReport());
// Update commands
const rest = new REST({version: '10'}).setToken(this.config.DISCORD_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 registerCommandsOnGuild({
rest,
guildId: guild.id,
applicationId: this.client.user!.id,
commands: this.commandsByName.map(c => c.slashCommand),
});
}),
// Remove commands registered on bot (if they exist)
rest.put(Routes.applicationCommands(this.client.user!.id), {body: []}),
],
);
}
this.client.user!.setPresence({
activities: [
{
name: this.config.BOT_ACTIVITY,
type: this.config.BOT_ACTIVITY_TYPE,
url: this.config.BOT_ACTIVITY_URL === '' ? undefined : this.config.BOT_ACTIVITY_URL,
},
],
status: this.config.BOT_STATUS,
});
spinner.succeed(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? ''}&scope=bot%20applications.commands&permissions=36700160`);
});
this.client.on('error', console.error);
this.client.on('debug', debug);
this.client.on('guildCreate', handleGuildCreate);
this.client.on('voiceStateUpdate', handleVoiceStateUpdate);
await this.client.login();
}
}
|