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

Annotation of /branches/8.x/src/services/SnippetManager.ts

Parent Directory Parent Directory | Revision Log Revision Log


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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26