1 |
/** |
2 |
* This file is part of SudoBot. |
3 |
* |
4 |
* Copyright (C) 2021-2023 OSN Developers. |
5 |
* |
6 |
* SudoBot is free software; you can redistribute it and/or modify it |
7 |
* under the terms of the GNU Affero General Public License as published by |
8 |
* the Free Software Foundation, either version 3 of the License, or |
9 |
* (at your option) any later version. |
10 |
* |
11 |
* SudoBot is distributed in the hope that it will be useful, but |
12 |
* WITHOUT ANY WARRANTY; without even the implied warranty of |
13 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 |
* GNU Affero General Public License for more details. |
15 |
* |
16 |
* You should have received a copy of the GNU Affero General Public License |
17 |
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>. |
18 |
*/ |
19 |
|
20 |
import { PrismaClient } from "@prisma/client"; |
21 |
import { Awaitable, ClientOptions, Collection, Client as DiscordClient, GuildEmoji, UserResolvable } from "discord.js"; |
22 |
import { Stats } from "fs"; |
23 |
import fs from "fs/promises"; |
24 |
import path, { basename, dirname } from "path"; |
25 |
import Server from "../api/Server"; |
26 |
import type AIAutoModService from "../automod/AIAutoModService"; |
27 |
import type Antijoin from "../automod/Antijoin"; |
28 |
import type Antiraid from "../automod/Antiraid"; |
29 |
import type Antispam from "../automod/Antispam"; |
30 |
import type FileFilterService from "../automod/FileFilterService"; |
31 |
import type MessageFilter from "../automod/MessageFilter"; |
32 |
import type MessageRuleService from "../automod/MessageRuleService"; |
33 |
import type ProfileFilter from "../automod/ProfileFilter"; |
34 |
import { SuppressErrorsMetadata } from "../decorators/SuppressErrors"; |
35 |
import type AFKService from "../services/AFKService"; |
36 |
import type AutoRoleService from "../services/AutoRoleService"; |
37 |
import type BallotManager from "../services/BallotManager"; |
38 |
import type BumpReminderService from "../services/BumpReminderService"; |
39 |
import type ChannelLockManager from "../services/ChannelLockManager"; |
40 |
import type CommandManager from "../services/CommandManager"; |
41 |
import CommandPermissionOverwriteManager from "../services/CommandPermissionOverwriteManager"; |
42 |
import type ConfigManager from "../services/ConfigManager"; |
43 |
import type CooldownService from "../services/CooldownService"; |
44 |
import type ExtensionService from "../services/ExtensionService"; |
45 |
import type InfractionManager from "../services/InfractionManager"; |
46 |
import type InviteTrackerService from "../services/InviteTrackerService"; |
47 |
import type KeypressHandlerService from "../services/KeypressHandlerService"; |
48 |
import type LogServer from "../services/LogServer"; |
49 |
import type LoggerService from "../services/LoggerService"; |
50 |
import type MetadataService from "../services/MetadataService"; |
51 |
import type PermissionManager from "../services/PermissionManager"; |
52 |
import type QueueManager from "../services/QueueManager"; |
53 |
import type QuickMuteService from "../services/QuickMuteService"; |
54 |
import type ReactionRoleService from "../services/ReactionRoleService"; |
55 |
import type SnippetManager from "../services/SnippetManager"; |
56 |
import type StartupManager from "../services/StartupManager"; |
57 |
import type TranslationService from "../services/TranslationService"; |
58 |
import type TriggerService from "../services/TriggerService"; |
59 |
import type WelcomerService from "../services/WelcomerService"; |
60 |
import { CustomEvents, type ClientEvents } from "../types/ClientEvents"; |
61 |
import { log, logError, logInfo } from "../utils/logger"; |
62 |
import { developmentMode } from "../utils/utils"; |
63 |
import type Command from "./Command"; |
64 |
import ServiceManager from "./ServiceManager"; |
65 |
|
66 |
export default class Client<Ready extends boolean = boolean> extends DiscordClient<Ready> { |
67 |
aliases = { |
68 |
"@services": path.resolve(__dirname, "../services"), |
69 |
"@automod": path.resolve(__dirname, "../automod") |
70 |
}; |
71 |
|
72 |
services = [ |
73 |
"@services/StartupManager", |
74 |
"@services/ConfigManager", |
75 |
"@services/CommandManager", |
76 |
"@services/InfractionManager", |
77 |
"@services/LoggerService", |
78 |
"@services/QueueManager", |
79 |
"@services/WelcomerService", |
80 |
"@services/SnippetManager", |
81 |
"@services/ChannelLockManager", |
82 |
"@services/PermissionManager", |
83 |
"@services/MetadataService", |
84 |
"@services/QuickMuteService", |
85 |
"@services/TranslationService", |
86 |
"@services/AutoRoleService", |
87 |
"@services/ReactionRoleService", |
88 |
"@services/AFKService", |
89 |
"@services/InviteTrackerService", |
90 |
"@services/BallotManager", |
91 |
"@services/TriggerService", |
92 |
"@services/ExtensionService", |
93 |
"@services/BumpReminderService", |
94 |
"@services/LogServer", |
95 |
"@services/CooldownService", |
96 |
"@services/KeypressHandlerService", |
97 |
"@services/CommandPermissionOverwriteManager", |
98 |
|
99 |
"@automod/MessageFilter", |
100 |
"@automod/Antispam", |
101 |
"@automod/Antiraid", |
102 |
"@automod/Antijoin", |
103 |
"@automod/ProfileFilter", |
104 |
"@automod/FileFilterService", |
105 |
"@automod/MessageRuleService", |
106 |
"@automod/AIAutoModService" |
107 |
]; |
108 |
|
109 |
commandsDirectory = path.resolve(__dirname, "../commands"); |
110 |
eventsDirectory = path.resolve(__dirname, "../events"); |
111 |
|
112 |
serviceManager = new ServiceManager(this); |
113 |
|
114 |
startupManager: StartupManager = {} as StartupManager; |
115 |
configManager: ConfigManager = {} as ConfigManager; |
116 |
commandManager: CommandManager = {} as CommandManager; |
117 |
infractionManager: InfractionManager = {} as InfractionManager; |
118 |
logger: LoggerService = {} as LoggerService; |
119 |
messageFilter: MessageFilter = {} as MessageFilter; |
120 |
antispam: Antispam = {} as Antispam; |
121 |
queueManager: QueueManager = {} as QueueManager; |
122 |
snippetManager: SnippetManager = {} as SnippetManager; |
123 |
welcomerService: WelcomerService = {} as WelcomerService; |
124 |
antiraid: Antiraid = {} as Antiraid; |
125 |
channelLockManager: ChannelLockManager = {} as ChannelLockManager; |
126 |
antijoin: Antijoin = {} as Antijoin; |
127 |
profileFilter: ProfileFilter = {} as ProfileFilter; |
128 |
permissionManager: PermissionManager = {} as PermissionManager; |
129 |
metadata: MetadataService = {} as MetadataService; |
130 |
quickMute: QuickMuteService = {} as QuickMuteService; |
131 |
translator: TranslationService = {} as TranslationService; |
132 |
autoRoleService: AutoRoleService = {} as AutoRoleService; |
133 |
reactionRoleService: ReactionRoleService = {} as ReactionRoleService; |
134 |
afkService: AFKService = {} as AFKService; |
135 |
inviteTracker: InviteTrackerService = {} as InviteTrackerService; |
136 |
ballotManager: BallotManager = {} as BallotManager; |
137 |
fileFilter: FileFilterService = {} as FileFilterService; |
138 |
messageRuleService: MessageRuleService = {} as MessageRuleService; |
139 |
triggerService: TriggerService = {} as TriggerService; |
140 |
aiAutoMod: AIAutoModService = {} as AIAutoModService; |
141 |
extensionService: ExtensionService = {} as ExtensionService; |
142 |
bumpReminder: BumpReminderService = {} as BumpReminderService; |
143 |
logServer: LogServer = {} as LogServer; |
144 |
cooldown: CooldownService = {} as CooldownService; |
145 |
keypressHandler: KeypressHandlerService = {} as KeypressHandlerService; |
146 |
commandPermissionOverwriteManager: CommandPermissionOverwriteManager = {} as CommandPermissionOverwriteManager; |
147 |
|
148 |
prisma = new PrismaClient({ |
149 |
errorFormat: "pretty", |
150 |
log: developmentMode() ? ["query", "error", "info", "warn"] : ["error", "info", "warn"] |
151 |
}); |
152 |
|
153 |
server = new Server(this); |
154 |
|
155 |
commands = new Collection<string, Command>(); |
156 |
emojiMap = new Collection<string, GuildEmoji>(); |
157 |
registeredEvents: string[] = []; |
158 |
eventMap = new Map<keyof ClientEvents, Function[]>(); |
159 |
|
160 |
private static _instance: Client; |
161 |
|
162 |
static get instance() { |
163 |
return this._instance; |
164 |
} |
165 |
|
166 |
constructor(options: ClientOptions, { services }: { services?: string[] } = {}) { |
167 |
super(options); |
168 |
Client._instance = this; |
169 |
|
170 |
if (services) { |
171 |
this.services = services; |
172 |
} |
173 |
} |
174 |
|
175 |
async boot() { |
176 |
await this.serviceManager.loadServices(); |
177 |
await this.extensionService.bootUp(); |
178 |
} |
179 |
|
180 |
async fetchUserSafe(user: UserResolvable) { |
181 |
try { |
182 |
return await this.users.fetch(user); |
183 |
} catch (e) { |
184 |
logError(e); |
185 |
return null; |
186 |
} |
187 |
} |
188 |
|
189 |
async loadCommands( |
190 |
directory = this.commandsDirectory, |
191 |
metadataLoader?: Function, |
192 |
filter?: (path: string, name: string, info: Stats) => Awaitable<boolean> |
193 |
) { |
194 |
const files = await fs.readdir(directory); |
195 |
const includeOnly = process.env.COMMANDS?.split(","); |
196 |
|
197 |
for (const file of files) { |
198 |
const filePath = path.join(directory, file); |
199 |
const info = await fs.lstat(filePath); |
200 |
const isDirectory = info.isDirectory(); |
201 |
|
202 |
if (isDirectory) { |
203 |
await this.loadCommands(filePath, metadataLoader, filter); |
204 |
continue; |
205 |
} |
206 |
|
207 |
if ((!file.endsWith(".ts") && !file.endsWith(".js")) || file.endsWith(".d.ts")) { |
208 |
continue; |
209 |
} |
210 |
|
211 |
if (includeOnly && !includeOnly.includes(file.replace(/\.(ts|js)/gi, ""))) { |
212 |
continue; |
213 |
} |
214 |
|
215 |
if (filter !== undefined && !(await filter(filePath, file, info))) { |
216 |
continue; |
217 |
} |
218 |
|
219 |
await this.loadCommand(filePath, metadataLoader); |
220 |
} |
221 |
} |
222 |
|
223 |
async loadCommand(filePath: string, metadataLoader?: Function) { |
224 |
const { default: CommandClass }: { default: new (client: Client) => Command } = await import(filePath); |
225 |
const command = new CommandClass(this); |
226 |
command.group = basename(dirname(filePath)); |
227 |
this.commands.set(command.name, command); |
228 |
|
229 |
for (const alias of command.aliases) { |
230 |
this.commands.set(alias, command); |
231 |
} |
232 |
|
233 |
if (!metadataLoader) { |
234 |
this.loadEventListenersFromMetadata(CommandClass, command); |
235 |
} else { |
236 |
metadataLoader(CommandClass, command); |
237 |
log("Custom metadata loader found"); |
238 |
} |
239 |
|
240 |
logInfo("Loaded command: ", command.name); |
241 |
} |
242 |
|
243 |
async loadEvents(directory = this.eventsDirectory) { |
244 |
const files = await fs.readdir(directory); |
245 |
|
246 |
for (const file of files) { |
247 |
const filePath = path.join(directory, file); |
248 |
const isDirectory = (await fs.lstat(filePath)).isDirectory(); |
249 |
|
250 |
if (isDirectory) { |
251 |
await this.loadEvents(filePath); |
252 |
continue; |
253 |
} |
254 |
|
255 |
if ((!file.endsWith(".ts") && !file.endsWith(".js")) || file.endsWith(".d.ts")) { |
256 |
continue; |
257 |
} |
258 |
|
259 |
await this.loadEvent(filePath, true); |
260 |
} |
261 |
} |
262 |
|
263 |
async loadEvent(filePath: string, addToList = false) { |
264 |
const { default: Event } = await import(filePath); |
265 |
const event = new Event(this); |
266 |
this.on(event.name, event.execute.bind(event)); |
267 |
|
268 |
if (addToList) { |
269 |
this.registeredEvents.push(event.name); |
270 |
} |
271 |
} |
272 |
|
273 |
private supressErrorMessagesHandler(suppressErrors: SuppressErrorsMetadata, e: unknown) { |
274 |
if (suppressErrors.mode === "log") { |
275 |
logError(e); |
276 |
} else if (suppressErrors.mode === "disabled") { |
277 |
throw e; |
278 |
} |
279 |
} |
280 |
|
281 |
loadEventListenersFromMetadata<I extends Object = Object>(Class: I["constructor"], instance?: I) { |
282 |
const metadata: { event: keyof ClientEvents; handler: Function; methodName: string }[] | undefined = Reflect.getMetadata( |
283 |
"event_listeners", |
284 |
Class.prototype |
285 |
); |
286 |
|
287 |
if (metadata) { |
288 |
for (const data of metadata) { |
289 |
const callback = instance ? data.handler.bind(instance) : data.handler; |
290 |
const suppressErrors: SuppressErrorsMetadata | undefined = Reflect.getMetadata( |
291 |
"supress_errors", |
292 |
Class.prototype, |
293 |
data.methodName |
294 |
); |
295 |
|
296 |
this.addEventListener( |
297 |
data.event, |
298 |
suppressErrors |
299 |
? (...args: any[]) => { |
300 |
try { |
301 |
const ret = callback(...args); |
302 |
|
303 |
if (ret instanceof Promise) ret.catch(e => this.supressErrorMessagesHandler(suppressErrors, e)); |
304 |
|
305 |
return ret; |
306 |
} catch (e) { |
307 |
this.supressErrorMessagesHandler(suppressErrors, e); |
308 |
} |
309 |
} |
310 |
: callback |
311 |
); |
312 |
} |
313 |
} |
314 |
} |
315 |
|
316 |
addEventListener<K extends keyof ClientEvents>(event: K, callback: (...args: ClientEvents[K]) => any) { |
317 |
const handlers = this.eventMap.get(event) ?? []; |
318 |
|
319 |
if (!this.eventMap.has(event) && !CustomEvents.includes(event)) { |
320 |
this.on(event as any, (...args: any[]) => this.fireEvent(event, ...args)); |
321 |
log("Registered parent handler for event: ", event); |
322 |
} |
323 |
|
324 |
handlers.push(callback); |
325 |
this.eventMap.set(event, handlers); |
326 |
|
327 |
log("Added event listener for event: ", event); |
328 |
} |
329 |
|
330 |
async emitWait<K extends keyof ClientEvents>(eventName: K, ...args: ClientEvents[K]) { |
331 |
const handlers = this.eventMap.get(eventName); |
332 |
|
333 |
if (handlers) { |
334 |
for (const handler of handlers) { |
335 |
await handler(...args); |
336 |
} |
337 |
} |
338 |
} |
339 |
|
340 |
async getHomeGuild() { |
341 |
const id = process.env.HOME_GUILD_ID; |
342 |
|
343 |
if (!id) { |
344 |
logError( |
345 |
"Environment variable `HOME_GUILD_ID` is not set. The bot can't work without it. Please follow the setup guide in the bot documentation." |
346 |
); |
347 |
process.exit(-1); |
348 |
} |
349 |
|
350 |
try { |
351 |
return this.guilds.cache.get(id) || (await this.guilds.fetch(id)); |
352 |
} catch (e) { |
353 |
logError(e); |
354 |
logError("Error fetching home guild: make sure the ID inside `HOME_GUILD_ID` environment variable is correct."); |
355 |
process.exit(-1); |
356 |
} |
357 |
} |
358 |
|
359 |
fireEvent(name: keyof ClientEvents, ...args: any[]) { |
360 |
const handlers = this.eventMap.get(name); |
361 |
|
362 |
if (handlers) { |
363 |
for (const handler of handlers) { |
364 |
handler(...args); |
365 |
} |
366 |
} |
367 |
} |
368 |
} |