-
Notifications
You must be signed in to change notification settings - Fork 59
/
Bot.ts
197 lines (177 loc) · 6.22 KB
/
Bot.ts
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import {
Client,
ClientEvents,
Collection,
Events,
IntentsBitField,
Interaction,
} from 'discord.js'
import { randomBytes } from 'node:crypto'
import { Environment } from './config'
import featurePlugins from './features'
import { BotContext } from './types/BotContext'
import { CommandConfig } from './types/CommandConfig'
import { Logger } from './types/Logger'
import { Plugin } from './types/Plugin'
import { RuntimeConfiguration } from './utils/RuntimeConfiguration'
import { createLogger } from './utils/createLogger'
export class Bot {
private nextContextId = Number.parseInt(randomBytes(3).toString('hex'), 16)
public readonly client = new Client({
intents: [
IntentsBitField.Flags.Guilds,
IntentsBitField.Flags.GuildMembers,
IntentsBitField.Flags.GuildMessages,
IntentsBitField.Flags.MessageContent,
],
})
private readonly runtimeConfiguration = new RuntimeConfiguration()
private readonly commands = new Collection<string, CommandConfig>()
private readonly isProduction = process.env.NODE_ENV === 'production'
private createBotContext(logPrefixes: string[]): BotContext {
return {
client: this.client,
runtimeConfiguration: this.runtimeConfiguration,
log: this.createNewLogger(logPrefixes),
}
}
private createNewLogger(
prefixes: string[],
contextId = this.getNextContextId(),
) {
return createLogger([`${contextId} |`, ...prefixes].join(' '))
}
private getNextContextId() {
return (this.nextContextId++ & 0xff_ff_ff).toString(16).padStart(6, '0')
}
async initAndStart() {
await this.init()
await this.client.login(Environment.BOT_TOKEN)
}
async init() {
const log = this.createNewLogger(['[Bot.init]'])
log.info(`Environment: ${this.isProduction ? 'Production' : 'Development'}`)
const initialRuntimeConfig = await this.runtimeConfiguration.init()
log.info(
'Initial runtime configuration: ' + JSON.stringify(initialRuntimeConfig),
)
this.client.once(Events.ClientReady, () => this.onReady())
this.client.on(Events.InteractionCreate, (interaction) =>
this.onInteractionCreate(interaction),
)
await this.loadPlugins(featurePlugins)
}
private async loadPlugins(plugins: Plugin[]) {
const log = this.createNewLogger(['[Bot.loadPlugins]'])
log.info('[PLUGIN] Setting up plugins...')
let initializers: (() => Promise<void>)[] | undefined = []
for (const plugin of plugins) {
const addInitializer = (init: () => Promise<void>) => {
if (!initializers) {
throw new Error(
`addInitializer() must be called synchronously in the plugin’s setup() (plugin: ${plugin.name})`,
)
}
initializers.push(init)
}
this.initPlugin(plugin, addInitializer, log)
}
const collectedInitializers = initializers
initializers = undefined
log.info('[PLUGIN] Running plugin initializers...')
await Promise.all(collectedInitializers.map((init) => init()))
}
private initPlugin(
plugin: Plugin,
addInitializer: (init: () => Promise<void>) => void,
log: Logger,
) {
const logPrefix = `[plugin=${plugin.name}]`
plugin.setup({
addCommand: (handler) => {
this.commands.set(handler.data.name, handler)
},
addEventHandler: (handler) => {
const logPrefixes = [logPrefix, `[event=${handler.eventName}]`]
const listener = async (
...arguments_: ClientEvents[typeof handler.eventName]
) => {
const botContext = this.createBotContext(logPrefixes)
try {
await handler.execute(botContext, ...arguments_)
} catch (error) {
botContext.log.error('Error in event handler', error)
}
}
if (handler.once) {
this.client.once(handler.eventName, listener)
} else {
this.client.on(handler.eventName, listener)
}
},
addInitializer: (init) => {
addInitializer(async () => {
const logPrefixes = [logPrefix, `[initializer]`]
return init(this.createBotContext(logPrefixes))
})
},
})
log.info(`Initialized ${plugin.name}`)
}
private async onReady() {
const { client, commands } = this
const log = this.createNewLogger(['[Bot.onReady]'])
log.info(`Now online as ${client.user?.tag}.`)
const commands_data = [...commands.values()].map((command) => command.data)
// Set guild commands
try {
const guild = client.guilds.cache.get(Environment.GUILD_ID)
if (!guild) {
throw new Error(`Guild ${Environment.GUILD_ID} not found`)
}
await guild.commands.set(commands_data)
log.info(`${commands.size} guild commands registered on ${guild.name}`)
} catch (error) {
console.error('Unable to set guild commands:', error)
}
// Clear global commands
try {
const commands = await client.application?.commands.fetch()
for (const command of commands?.values() || []) {
await command.delete()
console.info(`Deleted global command ${command.name}`)
}
} catch (error) {
console.error('Unable to clear application commands:', error)
}
}
private async onInteractionCreate(interaction: Interaction) {
const { commands } = this
if (interaction.isCommand()) {
const commandName = interaction.commandName
const command = commands.get(commandName)
if (!command) return
const botContext = this.createBotContext([`[command="${commandName}"]`])
try {
if (!command.disableAutoDeferReply) {
await interaction.deferReply({ ephemeral: command.ephemeral })
}
} catch (error) {
botContext.log.error(`Unable to defer reply`, error)
return
}
const started = Date.now()
const user = interaction.user
botContext.log.info(`Invoked by ${user.tag} (${user.id})`)
try {
await command.execute(botContext, interaction)
const time = Date.now() - started
botContext.log.info(`Finished in ${time}ms`)
} catch (error) {
const time = Date.now() - started
botContext.log.error(`Failed in ${time}ms`, error)
if (!command.disableAutoDeferReply) await interaction.deleteReply()
}
}
}
}