From b5e9d55b0b543110d7c93612c9292fed3384ce54 Mon Sep 17 00:00:00 2001 From: TheCodedProf Date: Sun, 29 Jan 2023 15:43:26 -0500 Subject: [PATCH] worked on scanners, database, tracks, some moving around and cleaning up files. --- TODO | 3 +- TODO.json | 3 +- src/commands/settings/rolemenu.ts | 24 +- src/commands/settings/tracks.ts | 395 ++++++++++++++++++++++++- src/commands/user/track.ts | 57 ++-- src/config/emojis.json | 2 +- src/reflex/scanners.ts | 32 +- src/utils/client.ts | 6 +- src/utils/createPageIndicator.ts | 19 ++ src/utils/database.ts | 27 ++ src/utils/ellipsis.ts | 4 + src/utils/performanceTesting/record.ts | 2 +- tsconfig.json | 2 +- 13 files changed, 503 insertions(+), 73 deletions(-) create mode 100644 src/utils/ellipsis.ts diff --git a/TODO b/TODO index 2ab95dc..4af4c22 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,3 @@ ? Role all Server rules -verificationRequired on welcome -// TODO !IMPORTANT! URL + image hash + file hash database +verificationRequired on welcome \ No newline at end of file diff --git a/TODO.json b/TODO.json index 8b211ef..4637953 100644 --- a/TODO.json +++ b/TODO.json @@ -21,6 +21,5 @@ "everyone": true, "roles": true } - }, - "tracks": [] + } } diff --git a/src/commands/settings/rolemenu.ts b/src/commands/settings/rolemenu.ts index 8796892..9528183 100644 --- a/src/commands/settings/rolemenu.ts +++ b/src/commands/settings/rolemenu.ts @@ -8,6 +8,7 @@ import getEmojiByName from "../../utils/getEmojiByName.js"; import createPageIndicator from "../../utils/createPageIndicator.js"; import { configToDropdown } from "../../actions/roleMenu.js"; import { modalInteractionCollector } from "../../utils/dualCollector.js"; +import ellipsis from "../../utils/ellipsis.js"; import lodash from 'lodash'; const isEqual = lodash.isEqual; @@ -44,8 +45,9 @@ const reorderRoleMenuPages = async (interaction: CommandInteraction, m: Message, .addComponents( new StringSelectMenuBuilder() .setCustomId("reorder") - .setPlaceholder("Select a page to move...") - .setMinValues(1) + .setPlaceholder("Select all pages in the order you want them to appear.") + .setMinValues(currentObj.length) + .setMaxValues(currentObj.length) .addOptions( currentObj.map((o, i) => new StringSelectMenuOptionBuilder() .setLabel(o.name) @@ -81,6 +83,7 @@ const reorderRoleMenuPages = async (interaction: CommandInteraction, m: Message, out = null; } if(!out) return; + out.deferUpdate(); if (out.isButton()) return; if(!out.values) return; const values = out.values; @@ -160,12 +163,7 @@ const editNameDescription = async (i: ButtonInteraction, interaction: StringSele } -const ellipsis = (str: string, max: number): string => { - if (str.length <= max) return str; - return str.slice(0, max - 3) + "..."; -} - -const createRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise => { +const editRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise => { if (!data) data = { name: "Role Menu Page", description: "A new role menu page", @@ -321,7 +319,7 @@ const callback = async (interaction: CommandInteraction): Promise => { let modified = false; do { const embed = new EmojiEmbed() - .setTitle("Role Menu Settings") + .setTitle("Role Menu") .setEmoji("GUILD.GREEN") .setStatus("Success"); const noRoleMenus = currentObject.length === 0; @@ -377,7 +375,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDisabled(!modified), ); if(noRoleMenus) { - embed.setDescription("No role menu page have been set up yet. Use the button below to add one.\n\n" + + embed.setDescription("No role menu pages have been set up yet. Use the button below to add one.\n\n" + createPageIndicator(1, 1, undefined, true) ); pageSelect.setDisabled(true); @@ -390,7 +388,7 @@ const callback = async (interaction: CommandInteraction): Promise => { page = Math.min(page, Object.keys(currentObject).length - 1); current = currentObject[page]!; embed.setDescription(`**Currently Editing:** ${current.name}\n\n` + - `**Description:** \`${current.description}\`\n` + + `**Description:**\n> ${current.description}\n` + `\n\n${createPageIndicator(Object.keys(config.roleMenu.options).length, page)}` ); @@ -424,7 +422,7 @@ const callback = async (interaction: CommandInteraction): Promise => { page++; break; case "add": - let newPage = await createRoleMenuPage(i, m) + let newPage = await editRoleMenuPage(i, m) if(!newPage) break; currentObject.push(); page = currentObject.length - 1; @@ -444,7 +442,7 @@ const callback = async (interaction: CommandInteraction): Promise => { case "action": switch(i.values[0]) { case "edit": - let edited = await createRoleMenuPage(i, m, current!); + let edited = await editRoleMenuPage(i, m, current!); if(!edited) break; currentObject[page] = edited; modified = true; diff --git a/src/commands/settings/tracks.ts b/src/commands/settings/tracks.ts index 0cad55c..782f52f 100644 --- a/src/commands/settings/tracks.ts +++ b/src/commands/settings/tracks.ts @@ -1,16 +1,405 @@ -import type { CommandInteraction, GuildMember, SlashCommandSubcommandBuilder } from "discord.js"; +import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, Collection, CommandInteraction, GuildMember, Message, ModalBuilder, ModalSubmitInteraction, Role, RoleSelectMenuBuilder, RoleSelectMenuInteraction, SlashCommandSubcommandBuilder, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import client from "../../utils/client.js"; - +import createPageIndicator, { createVerticalTrack } from "../../utils/createPageIndicator.js"; +import { LoadingEmbed } from "../../utils/defaults.js"; +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import getEmojiByName from "../../utils/getEmojiByName.js"; +import ellipsis from "../../utils/ellipsis.js"; +import { modalInteractionCollector } from "../../utils/dualCollector.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("tracks") .setDescription("Manage the tracks for the server") +interface ObjectSchema { + name: string; + retainPrevious: boolean; + nullable: boolean; + track: string[]; + manageableBy: string[]; +} + +const editName = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, current?: string) => { + + let name = current ?? ""; + const modal = new ModalBuilder() + .setTitle("Edit Name and Description") + .setCustomId("editNameDescription") + .addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Name") + .setCustomId("name") + .setPlaceholder("Name here...") // TODO: Make better placeholder + .setStyle(TextInputStyle.Short) + .setValue(name ?? "") + .setRequired(true) + ) + ) + const button = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + ) + + await i.showModal(modal) + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Tracks") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + ], + components: [button] + }); + + let out: ModalSubmitInteraction | null; + try { + out = await modalInteractionCollector( + m, + (m) => m.channel!.id === interaction.channel!.id, + (_) => true + ) as ModalSubmitInteraction | null; + } catch (e) { + console.error(e); + out = null; + } + if(!out) return name; + if (out.isButton()) return name; + if(!out.fields) return name; + name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name; + return name + +} + +const reorderTracks = async (interaction: ButtonInteraction, m: Message, roles: Collection, currentObj: string[]) => { + let reorderRow = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("reorder") + .setPlaceholder("Select all roles in the order you want users to gain them (Lowest to highest rank).") + .setMinValues(currentObj.length) + .setMaxValues(currentObj.length) + .addOptions( + currentObj.map((o, i) => new StringSelectMenuOptionBuilder() + .setLabel(roles.get(o)!.name) + .setValue(i.toString()) + ) + ) + ); + let buttonRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + ) + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Tracks") + .setDescription("Select all roles in the order you want users to gain them (Lowest to highest rank).") + .setStatus("Success") + ], + components: [reorderRow, buttonRow] + }); + let out: StringSelectMenuInteraction | ButtonInteraction | null; + try { + out = await m.awaitMessageComponent({ + filter: (i) => i.channel!.id === interaction.channel!.id, + time: 300000 + }) as StringSelectMenuInteraction | ButtonInteraction | null; + } catch (e) { + console.error(e); + out = null; + } + if(!out) return; + out.deferUpdate(); + if (out.isButton()) return; + if(!out.values) return; + const values = out.values; + + const newOrder: string[] = currentObj.map((_, i) => { + const index = values.findIndex(v => v === i.toString()); + return currentObj[index]; + }) as string[]; + + return newOrder; +} + +const editTrack = async (interaction: ButtonInteraction | StringSelectMenuInteraction, message: Message, roles: Collection, current?: ObjectSchema) => { + if(!current) { + current = { + name: "", + retainPrevious: false, + nullable: false, + track: [], + manageableBy: [] + } + } + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("edit") + .setLabel("Edit Name") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("reorder") + .setLabel("Reorder") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji), + ); + const roleSelect = new ActionRowBuilder() + .addComponents( + new RoleSelectMenuBuilder() + .setCustomId("addRole") + .setPlaceholder("Select a role to add") + ); + let closed = false; + do { + const editableRoles: string[] = current.track.map((r) => { + if(!(roles.get(r)!.position >= (interaction.member as GuildMember).roles.highest.position)) return r; + }).filter(v => v !== undefined) as string[]; + const selectMenu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("removeRole") + .setPlaceholder("Select a role to remove") + .addOptions( + editableRoles.map((r, i) => { + return new StringSelectMenuOptionBuilder() + .setLabel(r) + .setValue(i.toString())} + ) + ) + ); + let allowed: boolean[] = []; + for (const role of current.track) { + const disabled: boolean = + roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position; + allowed.push(disabled) + } + + const embed = new EmojiEmbed() + .setTitle("Tracks") + .setDescription( + `**Currently Editing:** ${current.name}\n\n` + + `${getEmojiByName} Members ${current.nullable ? "don't " : ""}need a role in this track` + + `${getEmojiByName} Members ${current.retainPrevious ? "don't " : ""}keep all roles below their current highest` + + createVerticalTrack(current.track, new Array(current.track.length).fill(false), allowed) + ) + + interaction.editReply({embeds: [embed], components: [buttons, roleSelect, selectMenu]}); + + let out: ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null; + + try { + out = await message.awaitMessageComponent({ + filter: (i) => i.channel!.id === interaction.channel!.id, + time: 300000 + }) as ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null; + } catch (e) { + console.error(e); + out = null; + } + + if(!out) return; + if (out.isButton()) { + out.deferUpdate(); + switch(out.customId) { + case "back": + closed = true; + break; + case "edit": + current.name = (await editName(out, interaction, message, current.name))!; + break; + case "reorder": + current.track = (await reorderTracks(out, message, roles, current.track))!; + } + } else if (out.isStringSelectMenu()) { + out.deferUpdate(); + switch(out.customId) { + case "removeRole": + const index = current.track.findIndex(v => v === editableRoles[parseInt((out! as StringSelectMenuInteraction).values![0]!)]); + current.track.splice(index, 1); + break; + } + } else { + switch(out.customId) { + case "addRole": + const role = out.values![0]!; + if(!current.track.includes(role)) { + current.track.push(role); + } + out.reply({content: "That role is already on this track", ephemeral: true}) + break; + } + } + + } while(!closed); + return current; +} const callback = async (interaction: CommandInteraction) => { - + const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true}) + const config = await client.database.guilds.read(interaction.guild!.id); + const tracks: ObjectSchema[] = config.tracks; + const roles = await interaction.guild!.roles.fetch(); + const memberRoles = interaction.member!.roles; + const member = interaction.member as GuildMember; + + let page = 0; + let closed = false; + let modified = false; + + do { + const embed = new EmojiEmbed() + .setTitle("Track Settings") + .setEmoji("TRACKS.ICON") + .setStatus("Success"); + const noTracks = config.tracks.length === 0; + let current: ObjectSchema; + + const pageSelect = new StringSelectMenuBuilder() + .setCustomId("page") + .setPlaceholder("Select a track to manage"); + const actionSelect = new StringSelectMenuBuilder() + .setCustomId("action") + .setPlaceholder("Perform an action") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Edit") + .setDescription("Edit this track") + .setValue("edit") + .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), + new StringSelectMenuOptionBuilder() + .setLabel("Delete") + .setDescription("Delete this track") + .setValue("delete") + .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji) + ); + const buttonRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) + .setDisabled(page === 0), + new ButtonBuilder() + .setCustomId("next") + .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Primary) + .setDisabled(page === Object.keys(tracks).length - 1), + new ButtonBuilder() + .setCustomId("add") + .setLabel("New Track") + .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Secondary) + .setDisabled(Object.keys(tracks).length >= 24), + new ButtonBuilder() + .setCustomId("save") + .setLabel("Save") + .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji) + .setStyle(ButtonStyle.Success) + .setDisabled(!modified), + ); + if(noTracks) { + embed.setDescription("No tracks have been set up yet. Use the button below to add one.\n\n" + + createPageIndicator(1, 1, undefined, true) + ); + pageSelect.setDisabled(true); + actionSelect.setDisabled(true); + pageSelect.addOptions(new StringSelectMenuOptionBuilder() + .setLabel("No tracks") + .setValue("none") + ); + } else { + page = Math.min(page, Object.keys(tracks).length - 1); + current = tracks[page]!; + embed.setDescription(`**Currently Editing:** ${current.name}\n\n` + + `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${current.nullable ? "don't " : ""}need a role in this track\n` + + `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${current.retainPrevious ? "" : "don't "}keep all roles below their current highest\n\n` + + createVerticalTrack(current.track, new Array(current.track.length).fill(false)) + + `\n${createPageIndicator(config.tracks.length, page)}` + ); + + pageSelect.addOptions( + tracks.map((key: ObjectSchema, index) => { + return new StringSelectMenuOptionBuilder() + .setLabel(ellipsis(key.name, 50)) + .setDescription(ellipsis(roles.get(key.track[0]!)?.name!, 50)) + .setValue(index.toString()); + }) + ); + + } + + await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder().addComponents(actionSelect), new ActionRowBuilder().addComponents(pageSelect), buttonRow]}); + let i: StringSelectMenuInteraction | ButtonInteraction; + try { + i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | StringSelectMenuInteraction; + } catch (e) { + closed = true; + break; + } + + await i.deferUpdate(); + if (i.isButton()) { + switch (i.customId) { + case "back": + page--; + break; + case "next": + page++; + break; + case "add": + let newPage = await editTrack(i, m, roles) + if(!newPage) break; + tracks.push(); + page = tracks.length - 1; + break; + case "save": + // client.database.guilds.write(interaction.guild!.id, {"roleMenu.options": tracks}); // TODO + modified = false; + break; + } + } else if (i.isStringSelectMenu()) { + switch (i.customId) { + case "action": + switch(i.values[0]) { + case "edit": + let edited = await editTrack(i, m, roles, current!); + if(!edited) break; + tracks[page] = edited; + modified = true; + break; + case "delete": + if(page === 0 && tracks.keys.length - 1 > 0) page++; + else page--; + tracks.splice(page, 1); + break; + } + break; + case "page": + page = parseInt(i.values[0]!); + break; + } + } + + } while (!closed) } diff --git a/src/commands/user/track.ts b/src/commands/user/track.ts index 25a784b..c7f441f 100644 --- a/src/commands/user/track.ts +++ b/src/commands/user/track.ts @@ -1,10 +1,11 @@ import { LoadingEmbed } from "../../utils/defaults.js"; -import Discord, { CommandInteraction, GuildMember, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, SelectMenuOptionBuilder, APIMessageComponentEmoji, StringSelectMenuBuilder, MessageComponentInteraction, StringSelectMenuInteraction } from "discord.js"; +import Discord, { CommandInteraction, GuildMember, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, APIMessageComponentEmoji, StringSelectMenuBuilder, MessageComponentInteraction, StringSelectMenuInteraction, StringSelectMenuOptionBuilder } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import addPlural from "../../utils/plurals.js"; import client from "../../utils/client.js"; +import { createVerticalTrack } from "../../utils/createPageIndicator.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder @@ -12,17 +13,8 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setDescription("Moves a user along a role track") .addUserOption((option) => option.setName("user").setDescription("The user to manage").setRequired(true)); -const generateFromTrack = (position: number, active: string | boolean, size: number, disabled: string | boolean) => { - active = active ? "ACTIVE" : "INACTIVE"; - disabled = disabled ? "GREY." : ""; - if (position === 0 && size === 1) return "TRACKS.SINGLE." + disabled + active; - if (position === size - 1) return "TRACKS.VERTICAL.BOTTOM." + disabled + active; - if (position === 0) return "TRACKS.VERTICAL.TOP." + disabled + active; - return "TRACKS.VERTICAL.MIDDLE." + disabled + active; -}; - const callback = async (interaction: CommandInteraction): Promise => { - const { renderUser } = client.logger; + const { renderUser, renderRole} = client.logger; const member = interaction.options.getMember("user") as GuildMember; const guild = interaction.guild; if (!guild) return; @@ -44,10 +36,10 @@ const callback = async (interaction: CommandInteraction): Promise => { const dropdown = new Discord.StringSelectMenuBuilder() .addOptions( config.tracks.map((option, index) => { - const hasRoleInTrack = option.track.some((element: string) => { + const hasRoleInTrack: boolean = option.track.some((element: string) => { return memberRoles.cache.has(element); }); - return new SelectMenuOptionBuilder({ + return new StringSelectMenuOptionBuilder({ default: index === track, label: option.name, value: index.toString(), @@ -68,33 +60,23 @@ const callback = async (interaction: CommandInteraction): Promise => { (data.retainPrevious ? "When promoted, the user keeps previous roles" : "Members will lose their current role when promoted") + "\n"; - generated += - "\n" + - data.track - .map((role, index) => { - const allow: boolean = - roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position && - !managed; - allowed.push(!allow); - return ( - getEmojiByName( - generateFromTrack(index, memberRoles.cache.has(role), data.track.length, allow) - ) + - " " + - roles.get(role)!.name + - " [<@&" + - roles.get(role)!.id + - ">]" - ); - }) - .join("\n"); + for (const role of data.track) { + const disabled: boolean = + roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position && !managed; + allowed.push(!disabled) + } + generated += "\n" + createVerticalTrack( + data.track.map((role) => renderRole(roles.get(role)!)), + data.track.map((role) => memberRoles.cache.has(role)), + allowed.map((allow) => !allow) + ); const selected = []; for (const position of data.track) { if (memberRoles.cache.has(position)) selected.push(position); } const conflict = data.retainPrevious ? false : selected.length > 1; let conflictDropdown: StringSelectMenuBuilder[] = []; - const conflictDropdownOptions: SelectMenuOptionBuilder[] = []; + const conflictDropdownOptions: StringSelectMenuOptionBuilder[] = []; let currentRoleIndex: number = -1; if (conflict) { generated += `\n\n${getEmojiByName(`PUNISH.WARN.${managed ? "YELLOW" : "RED"}`)} This user has ${ @@ -106,10 +88,9 @@ const callback = async (interaction: CommandInteraction): Promise => { "In order to promote or demote this user, you must select which role the member should keep."; selected.forEach((role) => { conflictDropdownOptions.push( - new SelectMenuOptionBuilder({ - label: roles.get(role)!.name, - value: roles.get(role)!.id - }) + new StringSelectMenuOptionBuilder() + .setLabel(roles.get(role)!.name) + .setValue(roles.get(role)!.id) ); }); conflictDropdown = [ diff --git a/src/config/emojis.json b/src/config/emojis.json index 1e62149..abeb52a 100644 --- a/src/config/emojis.json +++ b/src/config/emojis.json @@ -25,7 +25,7 @@ "ATTACHMENT": "997570687193587812", "LOGGING": "999613304446144562", "SAVE": "1065722246322200586", - "SHUFFLE": "1067913930304921690", + "SHUFFLE": "1069323453909454890", "NOTIFY": { "ON": "1000726394579464232", "OFF": "1000726363495477368" diff --git a/src/reflex/scanners.ts b/src/reflex/scanners.ts index f69156a..c8d59f0 100644 --- a/src/reflex/scanners.ts +++ b/src/reflex/scanners.ts @@ -1,10 +1,11 @@ import fetch from "node-fetch"; import FormData from "form-data"; -import { writeFileSync, createReadStream } from "fs"; +import fs, { writeFileSync, createReadStream } from "fs"; import generateFileName from "../utils/temp/generateFileName.js"; import Tesseract from "node-tesseract-ocr"; import type Discord from "discord.js"; import client from "../utils/client.js"; +import { createHash } from "crypto"; interface NSFWSchema { nsfw: boolean; @@ -14,7 +15,11 @@ interface MalwareSchema { } export async function testNSFW(link: string): Promise { - const p = await saveAttachment(link); + const [p, hash] = await saveAttachment(link); + console.log("Checking an image") + let alreadyHaveCheck = await client.database.scanCache.read(hash) + if(alreadyHaveCheck) return { nsfw: alreadyHaveCheck.data }; + console.log("Was not in db") const data = new FormData(); console.log(link); data.append("file", createReadStream(p)); @@ -32,13 +37,17 @@ export async function testNSFW(link: string): Promise { return { nsfw: false }; }); console.log(result); + client.database.scanCache.write(hash, result.nsfw); return { nsfw: result.nsfw }; } export async function testMalware(link: string): Promise { - const p = await saveAttachment(link); - const data = new FormData(); - data.append("file", createReadStream(p)); + const [p, hash] = await saveAttachment(link); + let alreadyHaveCheck = await client.database.scanCache.read(hash) + if(alreadyHaveCheck) return { safe: alreadyHaveCheck.data }; + const data = new URLSearchParams(); + let f = createReadStream(p); + data.append("file", f.read(fs.statSync(p).size)); console.log(link); const result = await fetch("https://unscan.p.rapidapi.com/malware", { method: "POST", @@ -54,12 +63,15 @@ export async function testMalware(link: string): Promise { return { safe: true }; }); console.log(result); + client.database.scanCache.write(hash, result.safe); return { safe: result.safe }; } export async function testLink(link: string): Promise<{ safe: boolean; tags: string[] }> { console.log(link); - const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/malware", { + let alreadyHaveCheck = await client.database.scanCache.read(link) + if(alreadyHaveCheck) return { safe: alreadyHaveCheck.data, tags: [] }; + const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/link", { method: "POST", headers: { "X-RapidAPI-Key": client.config.rapidApiKey, @@ -73,17 +85,18 @@ export async function testLink(link: string): Promise<{ safe: boolean; tags: str return { safe: true, tags: [] }; }); console.log(scanned); + client.database.scanCache.write(link, scanned.safe ?? true, []); return { safe: scanned.safe ?? true, tags: scanned.tags ?? [] }; } -export async function saveAttachment(link: string): Promise { +export async function saveAttachment(link: string): Promise<[string, string]> { const image = (await fetch(link)).arrayBuffer().toString(); const fileName = generateFileName(link.split("/").pop()!.split(".").pop()!); writeFileSync(fileName, image, "base64"); - return fileName; + return [fileName, createHash('sha512').update(image, 'base64').digest('base64')]; } const linkTypes = { @@ -139,8 +152,7 @@ export async function LinkCheck(message: Discord.Message): Promise { export async function NSFWCheck(element: string): Promise { try { - const test = await testNSFW(element); - return test.nsfw; + return (await testNSFW(element)).nsfw; } catch { return false; } diff --git a/src/utils/client.ts b/src/utils/client.ts index 2a0702a..41cdbca 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -2,7 +2,7 @@ import Discord, { Client, Interaction, AutocompleteInteraction, GatewayIntentBit import { Logger } from "../utils/log.js"; import Memory from "../utils/memory.js"; import type { VerifySchema } from "../reflex/verify.js"; -import { Guilds, History, ModNotes, Premium, PerformanceTest } from "../utils/database.js"; +import { Guilds, History, ModNotes, Premium, PerformanceTest, ScanCache } from "../utils/database.js"; import EventScheduler from "../utils/eventScheduler.js"; import type { RoleMenuSchema } from "../actions/roleMenu.js"; import config from "../config/main.json" assert { type: "json" }; @@ -22,6 +22,7 @@ class NucleusClient extends Client { premium: Premium; eventScheduler: EventScheduler; performanceTest: PerformanceTest; + scanCache: ScanCache; }; preloadPage: Record = {}; // e.g. { channelID: { command: privacy, page: 3}} commands: Record { + active = active ? "ACTIVE" : "INACTIVE"; + disabled = disabled ? "GREY." : ""; + if (position === 0 && size === 1) return "TRACKS.SINGLE." + disabled + active; + if (position === size - 1) return "TRACKS.VERTICAL.BOTTOM." + disabled + active; + if (position === 0) return "TRACKS.VERTICAL.TOP." + disabled + active; + return "TRACKS.VERTICAL.MIDDLE." + disabled + active; +}; + +export const createVerticalTrack = (items: string[], active: boolean[], disabled?: boolean[]) => { + let out = ""; + if (!disabled) disabled = new Array(items.length).fill(false); + for (let i = 0; i < items.length; i++) { + out += getEmojiByName(verticalTrackIndicator(i, active[i] ?? false, items.length, disabled[i] ?? false)); + out += items[i] + "\n"; + } + return out; +} + export default pageIndicator; diff --git a/src/utils/database.ts b/src/utils/database.ts index 10b0ddb..c7b1777 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -148,6 +148,33 @@ export class History { } } +interface ScanCacheSchema { + addedAt: Date; + hash: string; + data: boolean; + tags: string[]; +} + +export class ScanCache { + scanCache: Collection; + + constructor() { + this.scanCache = database.collection("scanCache"); + } + + async read(hash: string) { + return await this.scanCache.findOne({ hash: hash }); + } + + async write(hash: string, data: boolean, tags?: string[]) { + await this.scanCache.insertOne({ hash: hash, data: data, tags: tags ?? [], addedAt: new Date() }); // TODO: cleanup function maybe + } + + async cleanup() { + await this.scanCache.deleteMany({ addedAt: { $lt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 31)) }, hash: { $not$text: "http"} }); + } +} + export class PerformanceTest { performanceData: Collection; diff --git a/src/utils/ellipsis.ts b/src/utils/ellipsis.ts new file mode 100644 index 0000000..6ec5888 --- /dev/null +++ b/src/utils/ellipsis.ts @@ -0,0 +1,4 @@ +export default (str: string, max: number): string => { + if (str.length <= max) return str; + return str.slice(0, max - 3) + "..."; +} \ No newline at end of file diff --git a/src/utils/performanceTesting/record.ts b/src/utils/performanceTesting/record.ts index 95761e9..17cfb1e 100644 --- a/src/utils/performanceTesting/record.ts +++ b/src/utils/performanceTesting/record.ts @@ -39,7 +39,7 @@ const record = async () => { singleNotify( "performanceTest", config.developmentGuildID, - `Discord ping time: \`${results.discord}ms\`\nDatabase read time: \`${results.databaseRead}ms\`\nCPU usage: \`${results.resources.cpu}%\`\nMemory usage: \`${results.resources.memory}MB\`\nCPU temperature: \`${results.resources.temperature}°C\``, + `Discord ping time: \`${results.discord}ms\`\nDatabase read time: \`${results.databaseRead}ms\`\nCPU usage: \`${results.resources.cpu}%\`\nMemory usage: \`${Math.round(results.resources.memory)}MB\`\nCPU temperature: \`${results.resources.temperature}°C\``, "Critical", config.owners ) diff --git a/tsconfig.json b/tsconfig.json index a39c584..7e6abdc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ "skipLibCheck": true, "noImplicitReturns": false }, - "include": ["src/**/*"], + "include": ["src/**/*", "src/index.d.ts"], "exclude": ["src/Unfinished/**/*"] }