From 41d93567b8e1ec3166321d4289e3f7733e42f9e5 Mon Sep 17 00:00:00 2001 From: pineafan Date: Sat, 30 Jul 2022 22:10:15 +0100 Subject: [PATCH] Fixed welcome --- .gitignore | 6 +- ClicksMigratingProblems/index.js | 2 +- TODO | 2 +- TODO.json | 14 +- src/Unfinished/all.ts | 2 +- src/actions/tickets/create.ts | 1 + src/commands/createTestButton.ts | 35 ----- src/commands/help.ts | 2 +- src/commands/nucleus/invite.ts | 2 +- src/commands/nucleus/premium.ts | 2 +- src/commands/nucleus/stats.ts | 2 +- src/commands/settings/filters.ts | 29 ++++ src/commands/settings/logs/_meta.ts | 2 +- src/commands/settings/logs/events.ts | 95 +++++++++++- src/commands/settings/stats.ts | 7 +- src/commands/settings/tickets.ts | 121 ++++++++++++++++ src/commands/settings/verify.ts | 144 +++++++++++++++--- src/commands/settings/welcome.ts | 209 +++++++++++++++++++++++++++ src/commands/tag.ts | 2 +- src/commands/tags/create.ts | 2 +- src/config/default.json | 3 +- src/config/format.js | 9 +- src/events/commandError.ts | 2 +- src/events/interactionCreate.ts | 11 +- src/reflex/verify.ts | 1 + src/reflex/welcome.ts | 38 +++-- src/utils/database.ts | 4 +- src/utils/defaultEmbeds.ts | 3 +- src/utils/log.ts | 2 +- tsconfig.json | 2 +- 30 files changed, 641 insertions(+), 115 deletions(-) delete mode 100644 src/commands/createTestButton.ts create mode 100644 src/commands/settings/filters.ts create mode 100644 src/commands/settings/welcome.ts diff --git a/.gitignore b/.gitignore index 72a0e85..bfedc85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ dist/ .history/ node_modules/ -src/config/ +src/config/* +!src/config/format.ts +!src/config/default.json +!src/config/emojis.json +src/config/main.json .vscode/ yarn-error.log yarn.lock diff --git a/ClicksMigratingProblems/index.js b/ClicksMigratingProblems/index.js index ca6e63d..c6f77bf 100644 --- a/ClicksMigratingProblems/index.js +++ b/ClicksMigratingProblems/index.js @@ -62,7 +62,7 @@ for (const file of files) { "message": null, "role": null, }, - "welcomeRole": data.welcome ? (data.welcome.role !== null ? data.welcome.role.toString() : null) : null, + "role": data.welcome ? (data.welcome.role !== null ? data.welcome.role.toString() : null) : null, "channel": data.welcome ? (data.welcome.message.text !== null ? data.welcome.message.channel.toString() : null) : null, "message": data.welcome ? (data.welcome.message.text) : null }, diff --git a/TODO b/TODO index cdcd42a..579b574 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,6 @@ Role all Server rules -! A way to add buttons to verify / role menu +verificationRequired on welcome ROLE MENU SETTINGS diff --git a/TODO.json b/TODO.json index 7a13d1e..802aee6 100644 --- a/TODO.json +++ b/TODO.json @@ -1,11 +1,5 @@ { - "logging": { - "logs": { - "enabled": true, - "toLog": "3fffff" - } - }, "filters": { "images": { "NSFW": false, @@ -30,11 +24,5 @@ } }, "roleMenu": [], - "tracks": [], - "welcome": { - "enabled": false, - "welcomeRole": null, - "channel": null, - "message": null - } + "tracks": [] } \ No newline at end of file diff --git a/src/Unfinished/all.ts b/src/Unfinished/all.ts index 5c482f1..d49633c 100644 --- a/src/Unfinished/all.ts +++ b/src/Unfinished/all.ts @@ -78,7 +78,7 @@ const filterList = { } } -const callback = async (interaction: CommandInteraction) => { +const callback = async (interaction: CommandInteraction): Promise => { await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true}) let filters: Filter[] = [ filterList.member.has.role("959901346000154674"), diff --git a/src/actions/tickets/create.ts b/src/actions/tickets/create.ts index 722eb3f..28eb435 100644 --- a/src/actions/tickets/create.ts +++ b/src/actions/tickets/create.ts @@ -17,6 +17,7 @@ export default async function (interaction) { return await interaction.reply({embeds: [new EmojiEmbed() .setTitle("Tickets are disabled") .setDescription("Please enable tickets in the configuration to use this command.") + .setFooter({text: interaction.member.permissions.has("MANAGE_GUILD") ? "You can enable it by running /settings tickets" : ""}) .setStatus("Danger") .setEmoji("CONTROL.BLOCKCROSS") ], ephemeral: true}); diff --git a/src/commands/createTestButton.ts b/src/commands/createTestButton.ts deleted file mode 100644 index c04e13d..0000000 --- a/src/commands/createTestButton.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CommandInteraction, MessageActionRow, MessageButton } from "discord.js"; -import { SlashCommandBuilder } from "@discordjs/builders"; -import { WrappedCheck } from "jshaiku"; - -const command = new SlashCommandBuilder() - .setName("createtestbutton") - .setDescription("creates a test button") // TODO: remove for release - -const callback = (interaction: CommandInteraction) => { - interaction.reply({components: [new MessageActionRow().addComponents([ - new MessageButton() - .setCustomId("createticket") - .setLabel("Create Ticket") - .setStyle("PRIMARY") - .setDisabled(false), - new MessageButton() - .setCustomId("verifybutton") - .setLabel("Verify") - .setStyle("PRIMARY") - .setDisabled(false), - new MessageButton() - .setCustomId("rolemenu") - .setLabel("Get roles") - .setStyle("PRIMARY") - .setDisabled(false) - ])]}); -} - -const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => { - return true; -} - -export { command }; -export { callback }; -export { check }; \ No newline at end of file diff --git a/src/commands/help.ts b/src/commands/help.ts index 568a90c..c3b015c 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -6,7 +6,7 @@ const command = new SlashCommandBuilder() .setName("help") .setDescription("Shows help for commands") -const callback = (interaction: CommandInteraction) => { +const callback = async (interaction: CommandInteraction): Promise => { interaction.reply("hel p"); // TODO: FINISH THIS FOR RELEASE } diff --git a/src/commands/nucleus/invite.ts b/src/commands/nucleus/invite.ts index 7f7d4b8..96e1449 100644 --- a/src/commands/nucleus/invite.ts +++ b/src/commands/nucleus/invite.ts @@ -9,7 +9,7 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setName("invite") .setDescription("Invites Nucleus to your server") -const callback = (interaction: CommandInteraction) => { +const callback = async (interaction: CommandInteraction): Promise => { interaction.reply({embeds: [new EmojiEmbed() .setTitle("Invite") .setDescription("You can invite Nucleus to your server by clicking the button below") diff --git a/src/commands/nucleus/premium.ts b/src/commands/nucleus/premium.ts index 740ab7b..9d273b9 100644 --- a/src/commands/nucleus/premium.ts +++ b/src/commands/nucleus/premium.ts @@ -8,7 +8,7 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setName("premium") .setDescription("Information about Nucleus Premium") -const callback = (interaction: CommandInteraction) => { +const callback = async (interaction: CommandInteraction): Promise => { interaction.reply({embeds: [new EmojiEmbed() .setTitle("Premium") .setDescription( diff --git a/src/commands/nucleus/stats.ts b/src/commands/nucleus/stats.ts index cb10e7a..beea94b 100644 --- a/src/commands/nucleus/stats.ts +++ b/src/commands/nucleus/stats.ts @@ -9,7 +9,7 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setName("stats") .setDescription("Gets the bot's stats") -const callback = (interaction: CommandInteraction) => { +const callback = async (interaction: CommandInteraction): Promise => { interaction.reply({ embeds: [new EmojiEmbed() .setTitle("Stats") diff --git a/src/commands/settings/filters.ts b/src/commands/settings/filters.ts new file mode 100644 index 0000000..183b91c --- /dev/null +++ b/src/commands/settings/filters.ts @@ -0,0 +1,29 @@ +import { LoadingEmbed } from './../../utils/defaultEmbeds.js'; +import Discord, { CommandInteraction, MessageActionRow, MessageButton, MessageSelectMenu } from "discord.js"; +import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import { WrappedCheck } from "jshaiku"; +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import client from '../../utils/client.js'; +import confirmationMessage from '../../utils/confirmationMessage.js'; +import generateKeyValueList from '../../utils/generateKeyValueList.js'; +import { ChannelType } from 'discord-api-types'; +import getEmojiByName from '../../utils/getEmojiByName.js'; + +const command = (builder: SlashCommandSubcommandBuilder) => + builder + .setName("filter") + .setDescription("Setting for message filters") + +const callback = async (interaction: CommandInteraction): Promise => { + +} + +const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => { + let member = (interaction.member as Discord.GuildMember) + if (!member.permissions.has("MANAGE_GUILD")) throw "You must have the *Manage Server* permission to use this command" + return true; +} + +export { command }; +export { callback }; +export { check }; \ No newline at end of file diff --git a/src/commands/settings/logs/_meta.ts b/src/commands/settings/logs/_meta.ts index 15a6fd4..f46987f 100644 --- a/src/commands/settings/logs/_meta.ts +++ b/src/commands/settings/logs/_meta.ts @@ -1,4 +1,4 @@ -const name = "log"; +const name = "logs"; const description = "Settings for logging"; export { name, description }; \ No newline at end of file diff --git a/src/commands/settings/logs/events.ts b/src/commands/settings/logs/events.ts index dac200c..ef303cb 100644 --- a/src/commands/settings/logs/events.ts +++ b/src/commands/settings/logs/events.ts @@ -1,17 +1,106 @@ -import { CommandInteraction } from "discord.js"; +import { LoadingEmbed } from './../../../utils/defaultEmbeds.js'; +import Discord, { CommandInteraction, MessageActionRow, MessageButton, MessageSelectMenu } from "discord.js"; import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; import { WrappedCheck } from "jshaiku"; +import EmojiEmbed from "../../../utils/generateEmojiEmbed.js"; +import client from '../../../utils/client.js'; +import { toHexArray, toHexInteger } from "../../../utils/calculate.js"; + + +const logs = { + "channelUpdate": "Channels created, deleted or modified", + "emojiUpdate": "Server emojis modified", + "stickerUpdate": "Server stickers modified", + "guildUpdate": "Server settings updated", + "guildMemberUpdate": "Member updated (i.e. nickname)", + "guildMemberPunish": "Members punished (i.e. muted, banned, kicked)", + "guildRoleUpdate": "Role settings changed", + "guildInviteUpdate": "Server invite created or deleted", + "messageUpdate": "Message edited", + "messageDelete": "Message deleted", + "messageDeleteBulk": "Messages purged", + "messageReactionUpdate": "Message reactions cleared", + "messageMassPing": "Message pings multiple members at once", + "messageAnnounce": "Message published in announcement channel", + "threadUpdate": "Thread created or deleted", + "webhookUpdate": "Webhooks created or deleted", + "guildMemberVerify": "Member runs verify", + "autoModeratorDeleted": "Messages auto deleted by Nucleus", + "nucleusSettingsUpdated": "Nucleus' settings updated by a moderator", + "ticketUpdate": "Tickets created or deleted", +} const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("events") .setDescription("Sets what events should be logged") -const callback = (interaction: CommandInteraction) => { - interaction.reply("This command is not yet finished [settings/log/events]"); +const callback = async (interaction: CommandInteraction): Promise => { + await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true}); + let m; + while (true) { + let config = await client.database.guilds.read(interaction.guild.id) + let converted = toHexArray(config.logging.logs.toLog) + m = await interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Logging Events") + .setDescription("Below are the events being logged in the server. You can toggle them on and off in the dropdown") + .setStatus("Success") + .setEmoji("CHANNEL.TEXT.CREATE") + ], components: [ + new MessageActionRow().addComponents([new MessageSelectMenu() + .setPlaceholder("Set events to log") + .setMaxValues(Object.keys(logs).length) + .setCustomId("logs") + .setMinValues(0) + .setOptions(Object.keys(logs).map((e, i) => ({ + label: logs[e], + value: i.toString(), + default: converted.includes(e) + }))) + ]), + new MessageActionRow().addComponents([ + new MessageButton() + .setLabel("Select all") + .setStyle("PRIMARY") + .setCustomId("all"), + new MessageButton() + .setLabel("Select none") + .setStyle("DANGER") + .setCustomId("none") + ]) + ]}) + let i; + try { + i = await m.awaitMessageComponent({ time: 300000 }); + } catch (e) { + break + } + i.deferUpdate() + if (i.customId === "logs") { + let selected = i.values; + let newLogs = toHexInteger(selected.map(e => Object.keys(logs)[parseInt(e)])) + await client.database.guilds.write(interaction.guild.id, {"logging.logs.toLog": newLogs}) + } else if (i.customId === "all") { + let newLogs = toHexInteger(Object.keys(logs).map(e => e)) + await client.database.guilds.write(interaction.guild.id, {"logging.logs.toLog": newLogs}) + } else if (i.customId === "none") { + await client.database.guilds.write(interaction.guild.id, {"logging.logs.toLog": 0}) + } else { + break + } + } + m = await interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Logging Events") + .setDescription("Below are the events being logged in the server. You can toggle them on and off in the dropdown") + .setFooter({text: "Message timed out"}) + .setStatus("Success") + .setEmoji("CHANNEL.TEXT.CREATE") + ]}) } const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => { + let member = (interaction.member as Discord.GuildMember) + if (!member.permissions.has("MANAGE_GUILD")) throw "You must have the *Manage Server* permission to use this command" return true; } diff --git a/src/commands/settings/stats.ts b/src/commands/settings/stats.ts index d972674..be15869 100644 --- a/src/commands/settings/stats.ts +++ b/src/commands/settings/stats.ts @@ -144,12 +144,7 @@ const callback = async (interaction: CommandInteraction): Promise => { await client.database.guilds.write(interaction.guild.id, null, toRemove.map(k => `stats.${k}`)); } } - await interaction.editReply({embeds: [new EmojiEmbed() - .setTitle("Stats Channel") - .setDescription("The following channels update when someone joins or leaves the server. You can select a channel to remove it from the list.") - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ], components: []}) + await interaction.editReply({embeds: [m.embeds[0].setFooter({text: "Message closed"})], components: []}); } const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => { diff --git a/src/commands/settings/tickets.ts b/src/commands/settings/tickets.ts index f199ac3..44f974e 100644 --- a/src/commands/settings/tickets.ts +++ b/src/commands/settings/tickets.ts @@ -192,6 +192,11 @@ const callback = async (interaction: CommandInteraction): Promise => { .setEmoji(getEmojiByName("TICKETS.OTHER", "id")) .setStyle("SECONDARY") .setCustomId("manageTypes"), + new MessageButton() + .setLabel("Add create ticket button") + .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id")) + .setStyle("PRIMARY") + .setCustomId("send"), ])] }); let i; @@ -217,6 +222,122 @@ const callback = async (interaction: CommandInteraction): Promise => { await client.database.guilds.write(interaction.guild.id, null, ["tickets.supportRole"]) data.supportRole = undefined; } else lastClicked = "sup"; + } else if (i.component.customId === "send") { + const ticketMessages = [ + {label: "Create ticket", description: "Click the button below to create a ticket"}, + {label: "Issues, questions or feedback?", description: "Click below to open a ticket and get help from our staff team"}, + {label: "Contact Us", description: "Click the button below to speak to us privately"}, + ] + while (true) { + let enabled = data.enabled && data.category !== null; + await interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Ticket Button") + .setDescription("Select a message template to send in this channel") + .setFooter({text: enabled ? "" : "Tickets are not set up correctly so the button may not work for users. Check the main menu to find which options must be set."}) + .setStatus(enabled ? "Success" : "Warning") + .setEmoji("GUILD.ROLES.CREATE") + ], components: [ + new MessageActionRow().addComponents([ + new MessageSelectMenu().setOptions(ticketMessages.map((t: {label: string, description: string, value?: string}, index) => { + t.value = index.toString(); return t as {value: string, label: string, description: string} + })).setCustomId("template").setMaxValues(1).setMinValues(1).setPlaceholder("Select a message template"), + ]), + new MessageActionRow().addComponents([ + new MessageButton() + .setCustomId("back") + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle("DANGER"), + new MessageButton() + .setCustomId("blank") + .setLabel("Empty") + .setStyle("SECONDARY"), + new MessageButton() + .setCustomId("custom") + .setLabel("Custom") + .setEmoji(getEmojiByName("TICKETS.OTHER", "id")) + .setStyle("PRIMARY") + ]) + ]}); + let i; + try { + i = await m.awaitMessageComponent({time: 300000}); + } catch(e) { break } + if (i.component.customId === "template") { + i.deferUpdate() + await interaction.channel.send({embeds: [new EmojiEmbed() + .setTitle(ticketMessages[parseInt(i.values[0])].label) + .setDescription(ticketMessages[parseInt(i.values[0])].description) + .setStatus("Success") + .setEmoji("GUILD.TICKET.OPEN") + ], components: [new MessageActionRow().addComponents([new MessageButton() + .setLabel("Create Ticket") + .setEmoji(getEmojiByName("CONTROL.TICK", "id")) + .setStyle("SUCCESS") + .setCustomId("createticket") + ])]}); + break + } else if (i.component.customId === "blank") { + i.deferUpdate() + await interaction.channel.send({components: [new MessageActionRow().addComponents([new MessageButton() + .setLabel("Create Ticket") + .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id")) + .setStyle("SUCCESS") + .setCustomId("createticket") + ])]}); + break + } else if (i.component.customId === "custom") { + await i.showModal(new Discord.Modal().setCustomId("modal").setTitle(`Enter embed details`).addComponents( + new MessageActionRow().addComponents(new TextInputComponent() + .setCustomId("title") + .setLabel("Title") + .setMaxLength(256) + .setRequired(true) + .setStyle("SHORT") + ), + new MessageActionRow().addComponents(new TextInputComponent() + .setCustomId("description") + .setLabel("Description") + .setMaxLength(4000) + .setRequired(true) + .setStyle("PARAGRAPH") + ) + )) + await interaction.editReply({ + embeds: [new EmojiEmbed() + .setTitle("Ticket Button") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("GUILD.TICKET.OPEN") + ], components: [new MessageActionRow().addComponents([new MessageButton() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle("PRIMARY") + .setCustomId("back") + ])] + }); + let out; + try { + out = await modalInteractionCollector(m, (m) => m.channel.id === interaction.channel.id, (m) => m.customId === "modify") + } catch (e) { break } + if (out.fields) { + let title = out.fields.getTextInputValue("title"); + let description = out.fields.getTextInputValue("description"); + await interaction.channel.send({embeds: [new EmojiEmbed() + .setTitle(title) + .setDescription(description) + .setStatus("Success") + .setEmoji("GUILD.TICKET.OPEN") + ], components: [new MessageActionRow().addComponents([new MessageButton() + .setLabel("Create Ticket") + .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id")) + .setStyle("SUCCESS") + .setCustomId("createticket") + ])]}); + break + } else { continue } + } + } } else if (i.component.customId === "enabled") { await client.database.guilds.write(interaction.guild.id, { "tickets.enabled": !data.enabled }) data.enabled = !data.enabled; diff --git a/src/commands/settings/verify.ts b/src/commands/settings/verify.ts index 14dfe8d..a77f2f4 100644 --- a/src/commands/settings/verify.ts +++ b/src/commands/settings/verify.ts @@ -1,11 +1,12 @@ import { LoadingEmbed } from './../../utils/defaultEmbeds.js'; -import Discord, { CommandInteraction, MessageActionRow, MessageButton } from "discord.js"; +import Discord, { CommandInteraction, Emoji, MessageActionRow, MessageButton, MessageSelectMenu, TextInputComponent } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; import { WrappedCheck } from "jshaiku"; import client from "../../utils/client.js"; +import { modalInteractionCollector } from '../../utils/dualCollector.js'; const command = (builder: SlashCommandSubcommandBuilder) => builder @@ -53,7 +54,7 @@ const callback = async (interaction: CommandInteraction): Promise => { let data = { meta:{ type: 'verifyRoleChanged', - displayName: 'Ignored Groups Changed', + displayName: 'Verify Role Changed', calculateType: 'nucleusSettingsUpdated', color: NucleusColors.green, emoji: "CONTROL.BLOCKTICK", @@ -103,7 +104,12 @@ const callback = async (interaction: CommandInteraction): Promise => { .setLabel(clicks ? "Click again to confirm" : "Reset role") .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id")) .setStyle("DANGER") - .setDisabled(!role) + .setDisabled(!role), + new MessageButton() + .setCustomId("send") + .setLabel("Add verify button") + .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id")) + .setStyle("PRIMARY") ])]}); let i; try { @@ -117,23 +123,127 @@ const callback = async (interaction: CommandInteraction): Promise => { await client.database.guilds.write(interaction.guild.id, null, ["verify.role", "verify.enabled"]) role = undefined; } + } else if (i.component.customId === "send") { + const verifyMessages = [ + {label: "Verify", description: "Click the button below to get verified"}, + {label: "Get verified", description: "To get access to the rest of the server, click the button below"}, + {label: "Ready to verify?", description: "Click the button below to verify yourself"}, + ] + while (true) { + await interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Verify Button") + .setDescription("Select a message template to send in this channel") + .setFooter({text: role ? "" : "You do no have a verify role set so the button will not work."}) + .setStatus(role ? "Success" : "Warning") + .setEmoji("GUILD.ROLES.CREATE") + ], components: [ + new MessageActionRow().addComponents([ + new MessageSelectMenu().setOptions(verifyMessages.map((t: {label: string, description: string, value?: string}, index) => { + t.value = index.toString(); return t as {value: string, label: string, description: string} + })).setCustomId("template").setMaxValues(1).setMinValues(1).setPlaceholder("Select a message template"), + ]), + new MessageActionRow().addComponents([ + new MessageButton() + .setCustomId("back") + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle("DANGER"), + new MessageButton() + .setCustomId("blank") + .setLabel("Empty") + .setStyle("SECONDARY"), + new MessageButton() + .setCustomId("custom") + .setLabel("Custom") + .setEmoji(getEmojiByName("TICKETS.OTHER", "id")) + .setStyle("PRIMARY") + ]) + ]}); + let i; + try { + i = await m.awaitMessageComponent({time: 300000}); + } catch(e) { break } + if (i.component.customId === "template") { + i.deferUpdate() + await interaction.channel.send({embeds: [new EmojiEmbed() + .setTitle(verifyMessages[parseInt(i.values[0])].label) + .setDescription(verifyMessages[parseInt(i.values[0])].description) + .setStatus("Success") + .setEmoji("CONTROL.BLOCKTICK") + ], components: [new MessageActionRow().addComponents([new MessageButton() + .setLabel("Verify") + .setEmoji(getEmojiByName("CONTROL.TICK", "id")) + .setStyle("SUCCESS") + .setCustomId("verifybutton") + ])]}); + break + } else if (i.component.customId === "blank") { + i.deferUpdate() + await interaction.channel.send({components: [new MessageActionRow().addComponents([new MessageButton() + .setLabel("Verify") + .setEmoji(getEmojiByName("CONTROL.TICK", "id")) + .setStyle("SUCCESS") + .setCustomId("verifybutton") + ])]}); + break + } else if (i.component.customId === "custom") { + await i.showModal(new Discord.Modal().setCustomId("modal").setTitle(`Enter embed details`).addComponents( + new MessageActionRow().addComponents(new TextInputComponent() + .setCustomId("title") + .setLabel("Title") + .setMaxLength(256) + .setRequired(true) + .setStyle("SHORT") + ), + new MessageActionRow().addComponents(new TextInputComponent() + .setCustomId("description") + .setLabel("Description") + .setMaxLength(4000) + .setRequired(true) + .setStyle("PARAGRAPH") + ) + )) + await interaction.editReply({ + embeds: [new EmojiEmbed() + .setTitle("Verify Button") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("GUILD.TICKET.OPEN") + ], components: [new MessageActionRow().addComponents([new MessageButton() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle("PRIMARY") + .setCustomId("back") + ])] + }); + let out; + try { + out = await modalInteractionCollector(m, (m) => m.channel.id === interaction.channel.id, (m) => m.customId === "modify") + } catch (e) { break } + if (out.fields) { + let title = out.fields.getTextInputValue("title"); + let description = out.fields.getTextInputValue("description"); + await interaction.channel.send({embeds: [new EmojiEmbed() + .setTitle(title) + .setDescription(description) + .setStatus("Success") + .setEmoji("CONTROL.BLOCKTICK") + ], components: [new MessageActionRow().addComponents([new MessageButton() + .setLabel("Verify") + .setEmoji(getEmojiByName("CONTROL.TICK", "id")) + .setStyle("SUCCESS") + .setCustomId("verifybutton") + ])]}); + break + } else { continue } + } + } } else { - break + i.deferUpdate() + break; } } - await interaction.editReply({embeds: [new EmojiEmbed() - .setTitle("Verify Role") - .setDescription(role ? `Your verify role is currently set to <@&${role}}>` : `You have not set a verify role`) - .setStatus("Success") - .setEmoji("GUILD.ROLE.CREATE") - .setFooter({text: "Message closed"}) - ], components: [new MessageActionRow().addComponents([new MessageButton() - .setCustomId("clear") - .setLabel("Clear") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setStyle("SECONDARY") - .setDisabled(true), - ])]}); + await interaction.editReply({embeds: [m.embeds[0].setFooter({text: "Message closed"})], components: []}); } const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => { diff --git a/src/commands/settings/welcome.ts b/src/commands/settings/welcome.ts new file mode 100644 index 0000000..1a107ed --- /dev/null +++ b/src/commands/settings/welcome.ts @@ -0,0 +1,209 @@ +import { LoadingEmbed } from './../../utils/defaultEmbeds.js'; +import Discord, { CommandInteraction, MessageActionRow, MessageButton, MessageSelectMenu } from "discord.js"; +import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import { WrappedCheck } from "jshaiku"; +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import client from '../../utils/client.js'; +import confirmationMessage from '../../utils/confirmationMessage.js'; +import generateKeyValueList from '../../utils/generateKeyValueList.js'; +import { ChannelType } from 'discord-api-types'; +import getEmojiByName from '../../utils/getEmojiByName.js'; + +const command = (builder: SlashCommandSubcommandBuilder) => + builder + .setName("welcome") + .setDescription("Messages and roles sent or given when someone joins the server") + .addStringOption(option => option.setName("message").setDescription("The message to send when someone joins the server").setAutocomplete(true)) + .addRoleOption(option => option.setName("role").setDescription("The role given when someone joins the server")) + .addRoleOption(option => option.setName("ping").setDescription("The role pinged when someone joins the server")) + .addChannelOption(option => option.setName("channel").setDescription("The channel the welcome message should be sent to").addChannelTypes([ + ChannelType.GuildText, ChannelType.GuildNews + ])) + +const callback = async (interaction: CommandInteraction): Promise => { + const { renderRole, renderChannel, log, NucleusColors, entry, renderUser } = client.logger + await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true}); + let m; + if (interaction.options.getRole("role") || interaction.options.getChannel("channel") || interaction.options.getString("message")) { + let role; + let ping; + let message = interaction.options.getString("message"); + try { + role = interaction.options.getRole("role") + ping = interaction.options.getRole("ping") + } catch { + return await interaction.editReply({embeds: [new EmojiEmbed() + .setEmoji("GUILD.ROLES.DELETE") + .setTitle("Welcome Events") + .setDescription("The role you provided is not a valid role") + .setStatus("Danger") + ]}) + } + let channel; + try { + channel = interaction.options.getChannel("channel") + } catch { + return await interaction.editReply({embeds: [new EmojiEmbed() + .setEmoji("GUILD.ROLES.DELETE") + .setTitle("Welcome Events") + .setDescription("The channel you provided is not a valid channel") + .setStatus("Danger") + ]}) + } + role = role as Discord.Role + ping = ping as Discord.Role + channel = channel as Discord.TextChannel + let options = {} + if (role) options["role"] = renderRole(role) + if (ping) options["ping"] = renderRole(ping) + if (channel) options["channel"] = renderChannel(channel) + if (message) options["message"] = "\n> " + message + let confirmation = await new confirmationMessage(interaction) + .setEmoji("GUILD.ROLES.EDIT") + .setTitle("Welcome Events") + .setDescription(generateKeyValueList(options)) + .setColor("Warning") + .setInverted(true) + .send(true) + if (confirmation.cancelled) return + if (confirmation.success) { + try { + let toChange = {} + if (role) toChange["welcome.role"] = role.id + if (ping) toChange["welcome.ping"] = ping.id + if (channel) toChange["welcome.channel"] = channel.id + if (message) toChange["welcome.message"] = message + await client.database.guilds.write(interaction.guild.id, toChange); + let list = { + memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), + changedBy: entry(interaction.user.id, renderUser(interaction.user)) + } + if (role) list["role"] = entry(role.id, renderRole(role)) + if (ping) list["ping"] = entry(ping.id, renderRole(ping)) + if (channel) list["channel"] = entry(channel.id, renderChannel(channel.id)) + if (message) list["message"] = entry(message, `\`${message}\``) + try { + let data = { + meta:{ + type: 'welcomeSettingsUpdated', + displayName: 'Welcome Settings Changed', + calculateType: 'nucleusSettingsUpdated', + color: NucleusColors.green, + emoji: "CONTROL.BLOCKTICK", + timestamp: new Date().getTime() + }, + list: list, + hidden: { + guild: interaction.guild.id + } + } + log(data); + } catch {} + } catch (e) { + console.log(e) + return interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Welcome Events") + .setDescription(`Something went wrong while updating welcome settings`) + .setStatus("Danger") + .setEmoji("GUILD.ROLES.DELETE") + ], components: []}); + } + } else { + return interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Welcome Events") + .setDescription(`No changes were made`) + .setStatus("Success") + .setEmoji("GUILD.ROLES.CREATE") + ], components: []}); + } + } + let lastClicked = null + while (true) { + let config = await client.database.guilds.read(interaction.guild.id) + m = await interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Welcome Events") + .setDescription( + `**Message:** ${config.welcome.message ? `\n> ${config.welcome.message}` : "*None set*"}\n` + + `**Role:** ${config.welcome.role ? renderRole(await interaction.guild.roles.fetch(config.welcome.role)) : "*None set*"}\n` + + `**Ping:** ${config.welcome.ping ? renderRole(await interaction.guild.roles.fetch(config.welcome.ping)) : "*None set*"}\n` + + `**Channel:** ${config.welcome.channel ? (config.welcome.channel == "dm" ? "DM" : renderChannel(await interaction.guild.channels.fetch(config.welcome.channel))) : "*None set*"}` + ) + .setStatus("Success") + .setEmoji("CHANNEL.TEXT.CREATE") + ], components: [ + new MessageActionRow().addComponents([ + new MessageButton() + .setLabel(lastClicked == "clear-message" ? "Click again to confirm" : "Clear Message") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) + .setCustomId("clear-message") + .setDisabled(!config.welcome.message) + .setStyle("DANGER"), + new MessageButton() + .setLabel(lastClicked == "clear-role" ? "Click again to confirm" : "Clear Role") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) + .setCustomId("clear-role") + .setDisabled(!config.welcome.role) + .setStyle("DANGER"), + new MessageButton() + .setLabel(lastClicked == "clear-ping" ? "Click again to confirm" : "Clear Ping") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) + .setCustomId("clear-ping") + .setDisabled(!config.welcome.ping) + .setStyle("DANGER"), + new MessageButton() + .setLabel(lastClicked == "clear-channel" ? "Click again to confirm" : "Clear Channel") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) + .setCustomId("clear-channel") + .setDisabled(!config.welcome.channel) + .setStyle("DANGER"), + new MessageButton() + .setLabel("Set Channel to DM") + .setCustomId("set-channel-dm") + .setDisabled(config.welcome.channel == "dm") + .setStyle("SECONDARY") + ]) + ]}) + let i; + try { + i = await m.awaitMessageComponent({ time: 300000 }); + } catch (e) { + break + } + i.deferUpdate() + if (i.customId == "clear-message") { + if (lastClicked == "clear-message") { + await client.database.guilds.write(interaction.guild.id, {"welcome.message": null}); + lastClicked = null + } else { lastClicked = "clear-message" } + } else if (i.customId == "clear-role") { + if (lastClicked == "clear-role") { + await client.database.guilds.write(interaction.guild.id, {"welcome.role": null}); + lastClicked = null + } else { lastClicked = "clear-role" } + } else if (i.customId == "clear-ping") { + if (lastClicked == "clear-ping") { + await client.database.guilds.write(interaction.guild.id, {"welcome.ping": null}); + lastClicked = null + } else { lastClicked = "clear-ping" } + } else if (i.customId == "clear-channel") { + if (lastClicked == "clear-channel") { + await client.database.guilds.write(interaction.guild.id, {"welcome.channel": null}); + lastClicked = null + } else { lastClicked = "clear-channel" } + } else if (i.customId == "set-channel-dm") { + await client.database.guilds.write(interaction.guild.id, {"welcome.channel": "dm"}); + lastClicked = null + } + } + await interaction.editReply({embeds: [m.embeds[0].setFooter({text: "Message closed"})], components: []}); +} + +const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => { + let member = (interaction.member as Discord.GuildMember) + if (!member.permissions.has("MANAGE_GUILD")) throw "You must have the *Manage Server* permission to use this command" + return true; +} + +export { command }; +export { callback }; +export { check }; \ No newline at end of file diff --git a/src/commands/tag.ts b/src/commands/tag.ts index 03b13c6..70d5a75 100644 --- a/src/commands/tag.ts +++ b/src/commands/tag.ts @@ -9,7 +9,7 @@ const command = new SlashCommandBuilder() .setDescription("Get and manage the servers tags") .addStringOption(o => o.setName("tag").setDescription("The tag to get").setAutocomplete(true).setRequired(true)) -const callback = async (interaction: CommandInteraction) => { +const callback = async (interaction: CommandInteraction): Promise => { const config = await client.database.guilds.read(interaction.guild.id) const tags = config.getKey("tags") const tag = tags[interaction.options.getString("tag")] diff --git a/src/commands/tags/create.ts b/src/commands/tags/create.ts index b0a278d..4615def 100644 --- a/src/commands/tags/create.ts +++ b/src/commands/tags/create.ts @@ -79,7 +79,7 @@ const callback = async (interaction: CommandInteraction): Promise => { const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => { let member = (interaction.member as Discord.GuildMember) - if (!member.permissions.has("MANAGE_MESSAGES")) throw "You must have the *Manage Server* permission to use this command" + if (!member.permissions.has("MANAGE_MESSAGES")) throw "You must have the *Manage Messages* permission to use this command" return true; } diff --git a/src/config/default.json b/src/config/default.json index 84a4de0..3976812 100644 --- a/src/config/default.json +++ b/src/config/default.json @@ -29,7 +29,8 @@ }, "welcome": { "enabled": false, - "welcomeRole": null, + "role": null, + "ping": null, "channel": null, "message": null }, diff --git a/src/config/format.js b/src/config/format.js index f87c9f7..3d4af8d 100644 --- a/src/config/format.js +++ b/src/config/format.js @@ -61,7 +61,7 @@ export default async function(walkthrough = false) { if (walkthrough) { switch (key) { case "enableDevelopment": { - json[key] = (await getInput("\x1b[36mEnable development mode? \x1b[0m(\x1b[32mY\x1b[0m/\x1b[31mn\x1b[0m) > ") || "Y").toLowerCase() === "y"; break; + json[key] = (await getInput("\x1b[36mEnable development mode? This redisters commands in a single server making it easier to test\x1b[0m(\x1b[32mY\x1b[0m/\x1b[31mn\x1b[0m) > ") || "Y").toLowerCase() === "y"; break; } case "owners": { let chosen = "!"; let toWrite = [] @@ -80,7 +80,12 @@ export default async function(walkthrough = false) { } if (!json.mongoUrl.endsWith("/")) json.mongoUrl += "/"; if (!json.baseUrl.endsWith("/")) json.baseUrl += "/"; - let hosts = fs.readFileSync('/etc/hosts', 'utf8').toString().split("\n"); + let hosts; + try { + hosts = fs.readFileSync('/etc/hosts', 'utf8').toString().split("\n"); + } catch (e) { + return console.log("\x1b[31m⚠ No /etc/hosts found. Please ensure the file exists and is readable. (Windows is not supported, Mac and Linux users should not experience this error)") + } let localhost = hosts.find(line => line.split(" ")[1] === "localhost"); if (localhost) { localhost = localhost.split(" ")[0]; } else { localhost = "127.0.0.1" } diff --git a/src/events/commandError.ts b/src/events/commandError.ts index 6d3672e..8edb480 100644 --- a/src/events/commandError.ts +++ b/src/events/commandError.ts @@ -5,7 +5,7 @@ export const event = 'commandError' export async function callback(client, interaction, error) { if (interaction.replied || interaction.deferred) { await interaction.followUp({embeds: [new EmojiEmbed() - .setTitle("Something went") + .setTitle("Something went wrong") .setDescription(error.message ?? error.toString()) .setStatus("Danger") .setEmoji("CONTROL.BLOCKCROSS") diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index eaf4fea..a4bb8c3 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -16,8 +16,16 @@ function getAutocomplete(typed: string, options: string[]): object[] { return fuse.slice(0, 25).map(option => ({name: option.item, value: option.item})) } -const validReplacements = ["serverName", "memberCount", "memberCount:bots", "memberCount:humans"] function generateStatsChannelAutocomplete(typed) { + const validReplacements = ["serverName", "memberCount", "memberCount:bots", "memberCount:humans"] + let autocompletions = [] + const beforeLastOpenBracket = typed.match(/(.*){[^{}]{0,15}$/) + if (beforeLastOpenBracket !== null) { for (let replacement of validReplacements) { autocompletions.push(`${beforeLastOpenBracket[1]} {${replacement}}`) } } + else { for (let replacement of validReplacements) { autocompletions.push(`${typed} {${replacement}}`) } } + return getAutocomplete(typed, autocompletions) +} +function generateWelcomeMessageAutocomplete(typed) { + const validReplacements = ["serverName", "memberCount", "memberCount:bots", "memberCount:humans", "member:mention", "member:name"] let autocompletions = [] const beforeLastOpenBracket = typed.match(/(.*){[^{}]{0,15}$/) if (beforeLastOpenBracket !== null) { for (let replacement of validReplacements) { autocompletions.push(`${beforeLastOpenBracket[1]} {${replacement}}`) } } @@ -39,6 +47,7 @@ async function interactionCreate(interaction) { switch (`${interaction.commandName} ${interaction.options.getSubcommandGroup(false)} ${interaction.options.getSubcommand(false)}`) { case `tag null null`: { return interaction.respond(getAutocomplete(interaction.options.getString("tag"), (await tagAutocomplete(interaction)))) } case `settings null stats`: { return interaction.respond(generateStatsChannelAutocomplete(interaction.options.getString("name"))) } + case `settings null welcome`: { return interaction.respond(generateWelcomeMessageAutocomplete(interaction.options.getString("message"))) } } } } diff --git a/src/reflex/verify.ts b/src/reflex/verify.ts index b95d5db..bc7aa4f 100644 --- a/src/reflex/verify.ts +++ b/src/reflex/verify.ts @@ -17,6 +17,7 @@ export default async function(interaction) { if ((!config.verify.enabled ) || (!config.verify.role)) return interaction.editReply({embeds: [new EmojiEmbed() .setTitle("Verify") .setDescription(`Verify is not enabled on this server`) + .setFooter({text: interaction.member.permissions.has("MANAGE_GUILD") ? "You can enable it by running /settings verify" : ""}) .setStatus("Danger") .setEmoji("CONTROL.BLOCKCROSS") ], ephemeral: true, fetchReply: true}); diff --git a/src/reflex/welcome.ts b/src/reflex/welcome.ts index 5b80fbd..9be71d5 100644 --- a/src/reflex/welcome.ts +++ b/src/reflex/welcome.ts @@ -1,41 +1,39 @@ -import log from '../utils/log.js' import convertCurlyBracketString from '../utils/convertCurlyBracketString.js' import client from '../utils/client.js'; +import EmojiEmbed from '../utils/generateEmojiEmbed.js'; export async function callback(_, member) { if (member.bot) return let config = await client.database.guilds.read(member.guild.id); if (!config.welcome.enabled) return - if (!config.welcome.verificationRequired.role) { - if (config.welcome.welcomeRole) { - try { - await member.roles.add(config.welcome.welcomeRole) - } catch (err) { - console.error(err) - } - } - } - - if (!config.welcome.verificationRequired.message && config.welcome.channel) { + if (config.welcome.channel) { let string = config.welcome.message if (string) { string = await convertCurlyBracketString(string, member.id, member.displayName, member.guild.name, member.guild.members) - if (config.welcome.channel === 'dm') { try { - await member.send(string) - } catch (err) { - console.error(err) - } + await member.send({ + embeds: [new EmojiEmbed() + .setDescription(string) + .setStatus('Success') + ] + }) + } catch {} } else { - let channel = await member.client.channels.fetch(config.welcome.channel) + let channel = await member.guild.channels.fetch(config.welcome.channel) if (channel.guild.id !== member.guild.id) return if (!channel) return try { - await channel.send(string) + await channel.send({ + embeds: [new EmojiEmbed() + .setDescription(string) + .setStatus('Success') + ], + content: (config.welcome.ping ? `<@${config.welcome.ping}>` : '') + `<@${member.id}>` + }) } catch (err) { - console.error(err) + console.error(err) // SEN } } } diff --git a/src/utils/database.ts b/src/utils/database.ts index 2f04198..8a5ca6f 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -49,7 +49,6 @@ export class Guilds { let out = {$set: {}, $unset: {}} if (set) out["$set"] = set; if (unset.length) out["$unset"] = uo; - console.log(out) await this.guilds.updateOne({ id: guild }, out, { upsert: true }); } @@ -211,7 +210,8 @@ export interface GuildConfig { message: boolean, role: string | null }, - welcomeRole: string | null, + role: string | null, + ping: string | null, channel: string | null, message: string | null, } diff --git a/src/utils/defaultEmbeds.ts b/src/utils/defaultEmbeds.ts index 0f226da..e799307 100644 --- a/src/utils/defaultEmbeds.ts +++ b/src/utils/defaultEmbeds.ts @@ -4,5 +4,6 @@ export const LoadingEmbed = [new EmojiEmbed() .setTitle("Loading") .setDescription("One moment...") .setStatus("Danger") - .setEmoji("NUCLEUS.LOADING")] + .setEmoji("NUCLEUS.LOADING") +] diff --git a/src/utils/log.ts b/src/utils/log.ts index 22da837..0c6aa89 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -29,7 +29,7 @@ export class Logger { entry(value, displayValue) { return { value: value, displayValue: displayValue } } - renderChannel(channel: Discord.GuildChannel | Discord.ThreadChannel | Discord.NewsChannel) { + renderChannel(channel: Discord.GuildChannel | Discord.ThreadChannel) { return `${channel.name} [<#${channel.id}>]`; } renderRole(role: Discord.Role) { diff --git a/tsconfig.json b/tsconfig.json index e38a3bc..0b5e28f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,6 @@ "moduleResolution": "node", "skipLibCheck": true }, - "include": ["src/**/*", "isntaller.js"], + "include": ["src/**/*", "installer.js"], "exclude": [] } \ No newline at end of file