/[sudobot]/branches/6.x/src/services/ExtensionService.ts
ViewVC logotype

Contents of /branches/6.x/src/services/ExtensionService.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (show annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 15775 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
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 { Snowflake } from "discord.js";
21 import { existsSync } from "fs";
22 import fs from "fs/promises";
23 import path from "path";
24 import Client from "../core/Client";
25 import EventListener from "../core/EventListener";
26 import { Extension } from "../core/Extension";
27 import Service from "../core/Service";
28 import type { ClientEvents } from "../types/ClientEvents";
29 import { log, logError, logInfo, logWarn } from "../utils/logger";
30
31 export const name = "extensionService";
32
33 type Metadata = {
34 main?: string;
35 commands?: string;
36 services?: string;
37 events?: string;
38 language?: "typescript" | "javascript";
39 main_directory?: string;
40 build_command?: string;
41 };
42
43 const guildIdResolvers: Array<{
44 events: ReadonlyArray<keyof ClientEvents>;
45 resolver: (args: any) => Snowflake | null | undefined;
46 }> = [
47 {
48 events: ["applicationCommandPermissionsUpdate"],
49 resolver: ([data]: ClientEvents["applicationCommandPermissionsUpdate"]) => data.guildId
50 },
51 {
52 events: [
53 "autoModerationActionExecution",
54 "autoModerationRuleCreate",
55 "autoModerationRuleDelete",
56 "autoModerationRuleUpdate"
57 ],
58 resolver: ([data]: ClientEvents[
59 | "autoModerationActionExecution"
60 | "autoModerationRuleCreate"
61 | "autoModerationRuleDelete"
62 | "autoModerationRuleUpdate"]) => data?.guild.id ?? undefined
63 },
64 {
65 events: ["messageCreate", "messageDelete", "messageUpdate", "interactionCreate"],
66 resolver: ([data]: ClientEvents["messageCreate" | "messageDelete" | "messageUpdate" | "interactionCreate"]) =>
67 data?.guild?.id ?? data?.guildId ?? undefined
68 },
69 {
70 events: ["messageDeleteBulk"],
71 resolver: ([data]: ClientEvents["messageDeleteBulk"]) => data.first()?.guildId ?? undefined
72 },
73 {
74 events: ["channelCreate", "channelDelete", "channelUpdate", "channelPinsUpdate"],
75 resolver: ([data]: ClientEvents["channelCreate" | "channelDelete" | "channelUpdate" | "channelPinsUpdate"]) =>
76 data.isDMBased() ? undefined : data.guildId
77 },
78 {
79 events: ["emojiCreate", "emojiDelete", "emojiUpdate"],
80 resolver: ([data]: ClientEvents["emojiCreate" | "emojiDelete" | "emojiUpdate"]) => data?.guild?.id ?? undefined
81 },
82 {
83 events: ["messageReactionAdd", "messageReactionRemove", "messageReactionRemoveEmoji"],
84 resolver: ([data]: ClientEvents["messageReactionAdd" | "messageReactionRemove" | "messageReactionRemoveEmoji"]) =>
85 data?.message.guildId ?? undefined
86 },
87 {
88 events: ["messageReactionRemoveAll"],
89 resolver: ([data]: ClientEvents["messageReactionRemoveAll"]) => data?.guildId ?? undefined
90 },
91 {
92 events: ["guildAuditLogEntryCreate", "guildMembersChunk", "threadListSync"],
93 resolver: ([, data]: ClientEvents["guildAuditLogEntryCreate" | "guildMembersChunk" | "threadListSync"]) =>
94 data.id ?? undefined
95 },
96 {
97 events: ["guildAvailable", "guildCreate", "guildDelete", "guildUpdate", "guildUnavailable", "guildIntegrationsUpdate"],
98 resolver: ([data]: ClientEvents[
99 | "guildAvailable"
100 | "guildCreate"
101 | "guildUpdate"
102 | "guildUnavailable"
103 | "guildIntegrationsUpdate"]) => data.id ?? undefined
104 },
105 {
106 events: [
107 "guildBanAdd",
108 "guildBanRemove",
109 "guildMemberAdd",
110 "guildMemberRemove",
111 "guildMemberUpdate",
112 "guildMemberAvailable",
113 "inviteCreate",
114 "inviteDelete",
115 "roleCreate",
116 "roleDelete"
117 ],
118 resolver: ([data]: ClientEvents[
119 | "guildBanAdd"
120 | "guildBanRemove"
121 | "guildMemberAdd"
122 | "guildMemberRemove"
123 | "guildMemberUpdate"
124 | "guildMemberAvailable"
125 | "inviteCreate"
126 | "inviteDelete"
127 | "roleCreate"
128 | "roleDelete"]) => data.guild?.id ?? undefined
129 },
130 {
131 events: [
132 "guildScheduledEventCreate",
133 "guildScheduledEventDelete",
134 "guildScheduledEventUserAdd",
135 "guildScheduledEventUserRemove"
136 ],
137 resolver: ([data]: ClientEvents[
138 | "guildScheduledEventCreate"
139 | "guildScheduledEventDelete"
140 | "guildScheduledEventUserAdd"
141 | "guildScheduledEventUserRemove"]) => data.guild?.id ?? data.guildId ?? undefined
142 },
143 {
144 events: ["guildScheduledEventUpdate"],
145 resolver: ([data]: ClientEvents["guildScheduledEventUpdate"]) => data?.guild?.id ?? data?.guildId ?? undefined
146 },
147 {
148 events: ["presenceUpdate", "roleUpdate", "stageInstanceUpdate", "stickerUpdate", "threadUpdate", "voiceStateUpdate"],
149 resolver: ([data, data2]: ClientEvents[
150 | "presenceUpdate"
151 | "roleUpdate"
152 | "stageInstanceUpdate"
153 | "threadUpdate"
154 | "voiceStateUpdate"]) => data?.guild?.id ?? data2.guild?.id ?? undefined
155 },
156 {
157 events: ["stageInstanceDelete", "stageInstanceCreate", "stickerCreate", "stickerDelete", "threadCreate", "threadDelete"],
158 resolver: ([data]: ClientEvents[
159 | "stageInstanceDelete"
160 | "stageInstanceCreate"
161 | "stickerCreate"
162 | "stickerDelete"
163 | "threadCreate"
164 | "threadDelete"]) => data?.guild?.id ?? undefined
165 },
166 {
167 events: ["threadMemberUpdate"],
168 resolver: ([data, data2]: ClientEvents["threadMemberUpdate"]) =>
169 data?.guildMember?.guild.id ?? data2?.guildMember?.guild.id ?? undefined
170 },
171 {
172 events: ["typingStart", "webhookUpdate"],
173 resolver: ([data]: ClientEvents["typingStart" | "webhookUpdate"]) => data.guild?.id ?? undefined
174 },
175 {
176 events: ["command"],
177 resolver: ([, , , data]: ClientEvents["command"]) => data.guildId ?? undefined
178 },
179 {
180 events: [
181 "cacheSweep",
182 "debug",
183 "error",
184 "warn",
185 "invalidated",
186 "ready",
187 "shardReady",
188 "shardDisconnect",
189 "shardError",
190 "shardReconnecting",
191 "shardResume"
192 ],
193 resolver: () => null
194 }
195 ];
196
197 function getGuildIdResolversMap() {
198 const map = new Map<keyof ClientEvents, Function>();
199
200 for (const guildIdResolver of guildIdResolvers) {
201 for (const event of guildIdResolver.events) {
202 if (map.has(event)) {
203 logWarn(`Overlapping Guild ID Resolvers detected: `, event);
204 logWarn("This seems to be an internal bug. Please report this issue to the developers.");
205 }
206
207 map.set(event, guildIdResolver.resolver);
208 }
209 }
210
211 return map;
212 }
213
214 export default class ExtensionService extends Service {
215 protected readonly extensionsPath = path.join(__dirname, "../../extensions");
216 protected readonly guildIdResolvers = getGuildIdResolversMap();
217
218 async bootUp() {
219 if (!existsSync(this.extensionsPath)) {
220 log("No extensions found");
221 return;
222 }
223
224 const extensionsIndex = path.join(this.extensionsPath, "index.json");
225
226 if (existsSync(extensionsIndex)) {
227 await this.loadExtensionsFromIndex(extensionsIndex);
228 return;
229 }
230
231 await this.loadExtensions();
232 }
233
234 async loadExtensionsFromIndex(extensionsIndex: string) {
235 const { extensions } = JSON.parse(await fs.readFile(extensionsIndex, "utf-8"));
236
237 for (const { entry, commands, events, name, services } of extensions) {
238 logInfo("Loading extension (cached): ", name);
239
240 await this.loadExtension({
241 extensionPath: entry,
242 commands,
243 events,
244 extensionName: name,
245 services
246 });
247 }
248 }
249
250 async loadExtensions() {
251 const extensions = await fs.readdir(this.extensionsPath);
252
253 for (const extensionName of extensions) {
254 const extensionDirectory = path.resolve(this.extensionsPath, extensionName);
255 const isDirectory = (await fs.lstat(extensionDirectory)).isDirectory();
256
257 if (!isDirectory) {
258 continue;
259 }
260
261 logInfo("Loading extension: ", extensionName);
262 const metadataFile = path.join(extensionDirectory, "extension.json");
263
264 if (!existsSync(metadataFile)) {
265 logError(`Extension ${extensionName} does not have a "extension.json" file!`);
266 process.exit(-1);
267 }
268
269 const metadata: Metadata = JSON.parse(await fs.readFile(metadataFile, { encoding: "utf-8" }));
270 const {
271 main_directory = "./build",
272 commands = `./${main_directory}/commands`,
273 events = `./${main_directory}/events`,
274 services = `./${main_directory}/services`,
275 main = `./${main_directory}/index.js`
276 } = metadata;
277
278 await this.loadExtension({
279 extensionName,
280 extensionPath: path.join(extensionDirectory, main),
281 commandsDirectory: path.join(extensionDirectory, commands),
282 eventsDirectory: path.join(extensionDirectory, events),
283 servicesDirectory: path.join(extensionDirectory, services)
284 });
285 }
286 }
287
288 async loadExtension({
289 extensionPath,
290 commandsDirectory,
291 eventsDirectory,
292 commands,
293 events,
294 extensionName,
295 services,
296 servicesDirectory
297 }:
298 | {
299 extensionPath: string;
300 commandsDirectory: string;
301 eventsDirectory: string;
302 servicesDirectory: string;
303 extensionName: string;
304 commands?: never;
305 events?: never;
306 services?: never;
307 }
308 | {
309 extensionPath: string;
310 commandsDirectory?: never;
311 eventsDirectory?: never;
312 servicesDirectory?: never;
313 commands: string[];
314 events: string[];
315 services: string[];
316 extensionName: string;
317 }) {
318 const { default: ExtensionClass }: { default: new (client: Client) => Extension } = await import(extensionPath);
319 const extension = new ExtensionClass(this.client);
320 const commandPaths = await extension.commands();
321 const eventPaths = await extension.events();
322 const servicePaths = await extension.services();
323
324 if (servicePaths === null) {
325 if (servicesDirectory) {
326 if (existsSync(servicesDirectory)) {
327 await this.client.serviceManager.loadServiceFromDirectory(servicesDirectory);
328 }
329 } else if (services) {
330 for (const servicePath of services) {
331 await this.client.serviceManager.loadServiceFromFile(servicePath);
332 }
333 }
334 } else {
335 for (const servicePath of servicePaths) {
336 await this.client.serviceManager.loadServiceFromFile(servicePath);
337 }
338 }
339
340 if (commandPaths === null) {
341 if (commandsDirectory) {
342 if (existsSync(commandsDirectory)) {
343 await this.client.loadCommands(commandsDirectory);
344 }
345 } else if (commands) {
346 for (const commandPath of commands) {
347 await this.client.loadCommand(commandPath);
348 }
349 }
350 } else {
351 for (const commandPath of commandPaths) {
352 await this.client.loadCommand(commandPath);
353 }
354 }
355
356 if (eventPaths === null) {
357 if (eventsDirectory) {
358 if (existsSync(eventsDirectory)) {
359 await this.loadEvents(extensionName, eventsDirectory);
360 }
361 } else if (events) {
362 for (const eventPath of events) {
363 await this.loadEvent(extensionName, eventPath);
364 }
365 }
366 } else {
367 for (const eventPath of eventPaths) {
368 await this.loadEvent(extensionName, eventPath);
369 }
370 }
371 }
372
373 async loadEvents(extensionName: string, directory: string) {
374 const files = await fs.readdir(directory);
375
376 for (const file of files) {
377 const filePath = path.join(directory, file);
378 const isDirectory = (await fs.lstat(filePath)).isDirectory();
379
380 if (isDirectory) {
381 await this.loadEvents(extensionName, filePath);
382 continue;
383 }
384
385 if ((!file.endsWith(".ts") && !file.endsWith(".js")) || file.endsWith(".d.ts")) {
386 continue;
387 }
388
389 await this.loadEvent(extensionName, filePath);
390 }
391 }
392
393 async loadEvent(extensionName: string, filePath: string) {
394 const { default: Event }: { default: new (client: Client) => EventListener<keyof ClientEvents> } = await import(filePath);
395 const event = new Event(this.client);
396 this.client.addEventListener(event.name, this.wrapHandler(extensionName, event.name, event.execute.bind(event)));
397 }
398
399 wrapHandler<K extends keyof ClientEvents>(extensionName: string, eventName: K, handler: Function, bail?: boolean) {
400 return async (...args: ClientEvents[K]) => {
401 const guildId: Snowflake | null | undefined = this.guildIdResolvers.get(eventName)?.(args);
402
403 if (guildId === undefined) {
404 logError("Invalid event or failed to fetch guild: ", eventName);
405 return;
406 }
407
408 if (guildId !== null && !this.isEnabled(extensionName, guildId)) {
409 log("Extension isn't enabled in this guild: ", guildId);
410 return;
411 }
412
413 logInfo("Running: " + eventName + " [" + extensionName + "]");
414
415 try {
416 return await handler(...args);
417 } catch (e) {
418 logError(`Extension error: the extension '${extensionName}' seems to cause this exception`);
419 logError(e);
420
421 if (bail) {
422 return;
423 }
424 }
425 };
426 }
427
428 isEnabled(extensionName: string, guildId: Snowflake) {
429 const { disabled_extensions, enabled } = this.client.configManager.config[guildId]?.extensions ?? {};
430 const { default_mode } = this.client.configManager.systemConfig.extensions ?? {};
431 log(default_mode, enabled);
432 return (enabled === undefined ? default_mode === "enable_all" : enabled) && !disabled_extensions?.includes(extensionName);
433 }
434 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26