/[sudobot]/branches/7.x/src/services/SnippetManager.ts
ViewVC logotype

Contents of /branches/7.x/src/services/SnippetManager.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: 15525 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 { Snippet } from "@prisma/client";
21 import { AxiosError } from "axios";
22 import {
23 Attachment,
24 Collection,
25 GuildMember,
26 Message,
27 MessageCreateOptions,
28 PermissionFlagsBits,
29 PermissionsString,
30 Snowflake
31 } from "discord.js";
32 import { existsSync } from "fs";
33 import fs from "fs/promises";
34 import { basename } from "path";
35 import Command, { CommandMessage } from "../core/Command";
36 import Service from "../core/Service";
37 import EmbedSchemaParser from "../utils/EmbedSchemaParser";
38 import { downloadFile } from "../utils/download";
39 import { LogLevel, log, logError, logInfo, logWithLevel } from "../utils/logger";
40 import { getEmoji, sudoPrefix } from "../utils/utils";
41
42 export const name = "snippetManager";
43
44 export default class SnippetManager extends Service {
45 public readonly snippets = new Collection<`${Snowflake}_${string}`, Snippet>();
46
47 async boot() {
48 const snippets = await this.client.prisma.snippet.findMany();
49
50 for (const snippet of snippets) {
51 this.snippets.set(`${snippet.guild_id}_${snippet.name}`, snippet);
52 }
53 }
54
55 async removeFiles(fileNames: string[], guildId: string) {
56 for (const name of fileNames) {
57 const filePath = `${sudoPrefix(`storage/snippet_attachments/${guildId}`, true)}/${name}`;
58 logInfo("Attempting to remove file: " + filePath);
59 await fs.rm(filePath).catch(logError);
60 }
61 }
62
63 async createSnippet({
64 name,
65 content,
66 attachments,
67 guildId,
68 userId,
69 roles,
70 channels,
71 users,
72 randomize,
73 permissions,
74 permissionMode = "AND"
75 }: CreateSnippetOptions) {
76 if (!content && (!attachments || attachments.length === 0)) {
77 return { error: "Content or attachment is required to create a snippet" };
78 }
79
80 if (this.snippets.has(`${guildId}_${name}`)) {
81 return { error: "A snippet with the same name already exists" };
82 }
83
84 if (this.client.commands.has(name)) {
85 return { error: "Sorry, there's a built-in internal command already with that name" };
86 }
87
88 const filesDownloaded: string[] = [];
89
90 log(attachments);
91 log(this.client.configManager.systemConfig.snippets?.save_attachments);
92
93 if (attachments && this.client.configManager.systemConfig.snippets?.save_attachments) {
94 for (const attachment of attachments) {
95 const { error, name } = await this.downloadAttachment({
96 guildId,
97 proxyURL: attachment.proxyURL
98 });
99
100 if (error) {
101 await this.removeFiles(filesDownloaded, guildId);
102
103 if (error instanceof Error && error.message.startsWith("HTTP error")) {
104 return { error: error.message };
105 }
106
107 return { error: "Could not save attachments: An internal error has occurred" };
108 }
109
110 filesDownloaded.push(name);
111 }
112 }
113
114 const snippet = await this.client.prisma.snippet.create({
115 data: {
116 content: content ? [content] : undefined,
117 guild_id: guildId,
118 user_id: userId,
119 attachments: filesDownloaded,
120 channels,
121 roles,
122 users,
123 name,
124 randomize,
125 permissions,
126 permissionMode
127 }
128 });
129
130 this.snippets.set(`${guildId}_${snippet.name}`, snippet);
131
132 return { snippet };
133 }
134
135 async downloadAttachment({ guildId, proxyURL }: { proxyURL: string; guildId: string }) {
136 try {
137 const splittedName = basename(proxyURL).split(".");
138 const extension = splittedName.pop()!;
139 splittedName.push(`_${Date.now()}_${Math.random() * 10000000}`);
140 splittedName.push(extension);
141 const fileName = splittedName.join(".").replace(/\//g, "_");
142 const path = sudoPrefix(`storage/snippet_attachments/${guildId}`, true);
143
144 await downloadFile({
145 url: proxyURL,
146 path,
147 name: fileName
148 });
149
150 return { path, name: fileName };
151 } catch (e) {
152 logError(e);
153 return { error: e as Error | AxiosError };
154 }
155 }
156
157 async deleteSnippet({ name, guildId }: CommonSnippetActionOptions) {
158 if (!this.snippets.has(`${guildId}_${name}`)) {
159 return { error: "No snippet found with that name" };
160 }
161
162 await this.client.prisma.snippet.deleteMany({
163 where: {
164 guild_id: guildId,
165 name
166 }
167 });
168
169 await this.removeFiles(this.snippets.get(`${guildId}_${name}`)!.attachments, guildId);
170 this.snippets.delete(`${guildId}_${name}`);
171
172 return { success: true };
173 }
174
175 async renameSnippet({ name, guildId, newName }: CommonSnippetActionOptions & { newName: string }) {
176 if (!this.snippets.has(`${guildId}_${name}`)) {
177 return { error: "No snippet found with that name" };
178 }
179
180 if (this.client.commands.has(newName)) {
181 return { error: "Sorry, there's a built-in internal command already with that name" };
182 }
183
184 await this.client.prisma.snippet.updateMany({
185 where: {
186 name,
187 guild_id: guildId
188 },
189 data: {
190 name: newName
191 }
192 });
193
194 const snippet = this.snippets.get(`${guildId}_${name}`)!;
195 snippet.name = newName;
196 this.snippets.set(`${guildId}_${newName}`, snippet);
197 this.snippets.delete(`${guildId}_${name}`);
198
199 return { success: true, snippet };
200 }
201
202 async checkPermissions(snippet: Snippet, member: GuildMember, guildId: string, channelId?: string) {
203 if (member.permissions.has(PermissionFlagsBits.Administrator, true)) return true;
204
205 if (
206 (snippet.channels.length > 0 && channelId && !snippet.channels.includes(channelId)) ||
207 (snippet.users.length > 0 && !snippet.users.includes(member.user.id)) ||
208 (snippet.roles.length > 0 && !member.roles.cache.hasAll(...snippet.roles)) ||
209 (snippet.permissions.length > 0 &&
210 ((snippet.permissionMode === "AND" &&
211 !member.permissions.has(snippet.permissions as PermissionsString[], true)) ||
212 (snippet.permissionMode === "OR" &&
213 !member.permissions.any(snippet.permissions as PermissionsString[], true))))
214 ) {
215 log("Channel/user doesn't have permission to run this snippet.");
216 return false;
217 }
218
219 const { permissions: memberPermissions } = await this.client.permissionManager.getMemberPermissions(member, true);
220
221 if (this.client.permissionManager.usesLevelBasedMode(guildId) && typeof snippet.level === "number") {
222 const level = (await this.client.permissionManager.getManager(member.guild.id)).getPermissionLevel(member);
223
224 if (level < snippet.level) {
225 log("User doesn't have enough permission to run this snippet. (level based permission system)");
226 return false;
227 }
228 }
229
230 const { permissions: snippetPermissions } = await this.client.permissionManager.getMemberPermissions(member);
231
232 for (const permission of snippetPermissions) {
233 if (!memberPermissions.has(permission)) {
234 log("User doesn't have enough permission to run this snippet. (hybrid permission system)");
235 return false;
236 }
237 }
238
239 return true;
240 }
241
242 async createMessageOptionsFromSnippet({
243 name,
244 guildId,
245 channelId,
246 member
247 }: CommonSnippetActionOptions & {
248 channelId: string;
249 member: GuildMember;
250 }) {
251 if (!this.snippets?.has(`${guildId}_${name}`)) {
252 return { error: "No snippet found with that name", found: false };
253 }
254
255 const snippet = this.snippets.get(`${guildId}_${name}`)!;
256
257 if (!snippet.content && snippet.attachments.length === 0)
258 throw new Error("Corrupted database: snippet attachment and content both are unusable");
259
260 if (!(await this.checkPermissions(snippet, member, guildId, channelId))) {
261 return {
262 options: undefined
263 };
264 }
265
266 const files = [];
267
268 if (snippet.randomize && snippet.attachments.length > 0) {
269 const randomAttachment = snippet.attachments[Math.floor(Math.random() * snippet.attachments.length)];
270
271 const file = sudoPrefix(`storage/snippet_attachments/${guildId}/${randomAttachment}`, false);
272
273 if (!existsSync(file)) {
274 logWithLevel(LogLevel.CRITICAL, `Could find attachment: ${file}`);
275 } else {
276 files.push(file);
277 }
278 } else {
279 for (const attachment of snippet.attachments) {
280 const file = sudoPrefix(`storage/snippet_attachments/${guildId}/${attachment}`, false);
281
282 if (!existsSync(file)) {
283 logWithLevel(LogLevel.CRITICAL, `Could find attachment: ${file}`);
284 continue;
285 }
286
287 files.push(file);
288 }
289 }
290
291 const content = snippet.content[snippet.randomize ? Math.floor(Math.random() * snippet.content.length) : 0];
292
293 return {
294 options: EmbedSchemaParser.getMessageOptions(
295 {
296 content: content ?? undefined,
297 files
298 } as MessageCreateOptions,
299 true
300 ),
301 found: true
302 };
303 }
304
305 async onMessageCreate(message: Message, commandName: string) {
306 const { options, found } = await this.createMessageOptionsFromSnippet({
307 name: commandName,
308 guildId: message.guildId!,
309 channelId: message.channelId!,
310 member: message.member! as GuildMember
311 });
312
313 if (!found || !options) {
314 log("Snippet not found or permission error");
315 return false;
316 }
317
318 await message.channel.send(options).catch(logError);
319 return true;
320 }
321
322 async toggleRandomization({ guildId, name }: CommonSnippetActionOptions) {
323 if (!this.snippets.has(`${guildId}_${name}`)) return { error: "Snippet does not exist" };
324
325 const snippet = this.snippets.get(`${guildId}_${name}`);
326
327 if (!snippet) {
328 return { error: "Snippet does not exist" };
329 }
330
331 await this.client.prisma.snippet.update({
332 where: {
333 id: snippet.id
334 },
335 data: {
336 randomize: !snippet.randomize
337 }
338 });
339
340 const localSnippet = this.snippets.get(`${guildId}_${name}`)!;
341 localSnippet.randomize = !snippet.randomize;
342 this.snippets.set(`${guildId}_${name}`, localSnippet);
343
344 return { randomization: !snippet.randomize };
345 }
346
347 async pushFile({ files, guildId, name }: CommonSnippetActionOptions & { files: string[] }) {
348 if (!this.snippets.has(`${guildId}_${name}`)) return { error: "Snippet does not exist" };
349
350 const filesDownloaded = [];
351
352 for (const file of files) {
353 const { error, name } = await this.downloadAttachment({
354 guildId,
355 proxyURL: file
356 });
357
358 if (error) {
359 await this.removeFiles(filesDownloaded, guildId);
360
361 if (error instanceof Error && error.message.startsWith("HTTP error")) {
362 return { error: error.message };
363 }
364
365 return { error: "Could not save attachments: An internal error has occurred" };
366 }
367
368 filesDownloaded.push(name);
369 }
370
371 const { count } = await this.client.prisma.snippet.updateMany({
372 where: {
373 name,
374 guild_id: guildId
375 },
376 data: {
377 attachments: {
378 push: filesDownloaded
379 }
380 }
381 });
382
383 if (count === 0) return { error: "Snippet does not exist" };
384
385 const snippet = this.snippets.get(`${guildId}_${name}`)!;
386 snippet.attachments.push(...filesDownloaded);
387 this.snippets.set(`${guildId}_${name}`, snippet);
388
389 return { count };
390 }
391
392 async checkPermissionInSnippetCommands(name: string, message: CommandMessage, command: Command) {
393 const snippet = this.snippets.get(`${message.guildId!}_${name}`);
394
395 if (!snippet) {
396 await command.deferredReply(message, `${getEmoji(this.client, "error")} No snippet found with that name!')}`);
397 return false;
398 }
399
400 if (!(await this.client.snippetManager.checkPermissions(snippet, message.member! as GuildMember, message.guildId!))) {
401 await command.deferredReply(
402 message,
403 `${getEmoji(this.client, "error")} You don't have permission to modify this snippet!`
404 );
405 return false;
406 }
407
408 return true;
409 }
410
411 async updateSnippet({
412 channels,
413 content,
414 guildId,
415 name,
416 randomize,
417 roles,
418 users,
419 level
420 }: Partial<Omit<CreateSnippetOptions, "attachments" | "userId">> & CommonSnippetActionOptions & { level?: number }) {
421 if (!this.snippets.has(`${guildId}_${name}`)) {
422 return { error: "No snippet found with that name" };
423 }
424
425 const snippet = this.snippets.get(`${guildId}_${name}`)!;
426
427 const updatedSnippet = await this.client.prisma.snippet.update({
428 where: {
429 id: snippet.id
430 },
431 data: {
432 content: content ? [content] : undefined,
433 channels,
434 roles,
435 randomize,
436 users,
437 level
438 }
439 });
440
441 this.snippets.set(`${guildId}_${name}`, updatedSnippet);
442
443 return {
444 updatedSnippet
445 };
446 }
447 }
448
449 interface CommonSnippetActionOptions {
450 name: string;
451 guildId: string;
452 }
453
454 interface CreateSnippetOptions extends CommonSnippetActionOptions {
455 content?: string;
456 attachments?: Attachment[];
457 userId: string;
458 roles: string[];
459 users: string[];
460 channels: string[];
461 randomize?: boolean;
462 permissions?: PermissionsString[];
463 permissionMode?: "AND" | "OR";
464 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26