worked on scanners, database, tracks, some moving around and cleaning up files.
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 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 @@
.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 @@
out = null;
}
if(!out) return;
+ out.deferUpdate();
if (out.isButton()) return;
if(!out.values) return;
const values = out.values;
@@ -160,12 +163,7 @@
}
-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<ObjectSchema | null> => {
+const editRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise<ObjectSchema | null> => {
if (!data) data = {
name: "Role Menu Page",
description: "A new role menu page",
@@ -321,7 +319,7 @@
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 @@
.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 @@
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 @@
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 @@
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<TextInputBuilder>()
+ .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<ButtonBuilder>()
+ .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<string, Role>, currentObj: string[]) => {
+ let reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .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<ButtonBuilder>()
+ .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<string, Role>, current?: ObjectSchema) => {
+ if(!current) {
+ current = {
+ name: "",
+ retainPrevious: false,
+ nullable: false,
+ track: [],
+ manageableBy: []
+ }
+ }
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .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<RoleSelectMenuBuilder>()
+ .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<StringSelectMenuBuilder>()
+ .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<ButtonBuilder>()
+ .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<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().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 @@
.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<unknown> => {
- 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 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 @@
(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 @@
"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 @@
}
export async function testNSFW(link: string): Promise<NSFWSchema> {
- 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 @@
return { nsfw: false };
});
console.log(result);
+ client.database.scanCache.write(hash, result.nsfw);
return { nsfw: result.nsfw };
}
export async function testMalware(link: string): Promise<MalwareSchema> {
- 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 @@
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 @@
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<string> {
+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 NSFWCheck(element: string): Promise<boolean> {
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 { 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 @@
premium: Premium;
eventScheduler: EventScheduler;
performanceTest: PerformanceTest;
+ scanCache: ScanCache;
};
preloadPage: Record<string, {command: string, argument: string}> = {}; // e.g. { channelID: { command: privacy, page: 3}}
commands: Record<string, [{
@@ -51,7 +52,8 @@
notes: new ModNotes(),
premium: new Premium(),
eventScheduler: new EventScheduler(),
- performanceTest: new PerformanceTest()
+ performanceTest: new PerformanceTest(),
+ scanCache: new ScanCache()
});
export default client;
diff --git a/src/utils/createPageIndicator.ts b/src/utils/createPageIndicator.ts
index 4ddbae2..29ea83b 100644
--- a/src/utils/createPageIndicator.ts
+++ b/src/utils/createPageIndicator.ts
@@ -21,4 +21,23 @@
return out;
}
+export const verticalTrackIndicator = (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;
+};
+
+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 @@
}
}
+interface ScanCacheSchema {
+ addedAt: Date;
+ hash: string;
+ data: boolean;
+ tags: string[];
+}
+
+export class ScanCache {
+ scanCache: Collection<ScanCacheSchema>;
+
+ constructor() {
+ this.scanCache = database.collection<ScanCacheSchema>("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<PerformanceDataSchema>;
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 @@
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/**/*"]
}